##// END OF EJS Templates
feat(2fa): added 2fa for more auth plugins and moved 2fa forced functionality to ee edition. Fixes: RCCE-68
ilin.s -
r5397:46138ab9 default
parent child Browse files
Show More
@@ -1,987 +1,984 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':
180 return
181
182 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
179 if user_obj.needs_2fa_configure and view_name != self.SETUP_2FA_VIEW:
183 h.flash(
180 h.flash(
184 "You are required to configure 2FA",
181 "You are required to configure 2FA",
185 "warning",
182 "warning",
186 ignore_duplicate=False,
183 ignore_duplicate=False,
187 )
184 )
188 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
185 raise HTTPFound(self.request.route_path(self.SETUP_2FA_VIEW))
189
186
190 def _maybe_needs_2fa_check(self, view_name, user_obj):
187 def _maybe_needs_2fa_check(self, view_name, user_obj):
191 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
188 if view_name in self.DONT_CHECKOUT_VIEWS + self.EXTRA_VIEWS_TO_IGNORE:
192 return
189 return
193
190
194 if not user_obj:
191 if not user_obj:
195 return
192 return
196
193
197 if user_obj.check_2fa_required and view_name != self.VERIFY_2FA_VIEW:
194 if user_obj.check_2fa_required and view_name != self.VERIFY_2FA_VIEW:
198 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
195 raise HTTPFound(self.request.route_path(self.VERIFY_2FA_VIEW))
199
196
200 def _log_creation_exception(self, e, repo_name):
197 def _log_creation_exception(self, e, repo_name):
201 _ = self.request.translate
198 _ = self.request.translate
202 reason = None
199 reason = None
203 if len(e.args) == 2:
200 if len(e.args) == 2:
204 reason = e.args[1]
201 reason = e.args[1]
205
202
206 if reason == "INVALID_CERTIFICATE":
203 if reason == "INVALID_CERTIFICATE":
207 log.exception("Exception creating a repository: invalid certificate")
204 log.exception("Exception creating a repository: invalid certificate")
208 msg = _("Error creating repository %s: invalid certificate") % repo_name
205 msg = _("Error creating repository %s: invalid certificate") % repo_name
209 else:
206 else:
210 log.exception("Exception creating a repository")
207 log.exception("Exception creating a repository")
211 msg = _("Error creating repository %s") % repo_name
208 msg = _("Error creating repository %s") % repo_name
212 return msg
209 return msg
213
210
214 def _get_local_tmpl_context(self, include_app_defaults=True):
211 def _get_local_tmpl_context(self, include_app_defaults=True):
215 c = TemplateArgs()
212 c = TemplateArgs()
216 c.auth_user = self.request.user
213 c.auth_user = self.request.user
217 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
214 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
218 c.rhodecode_user = self.request.user
215 c.rhodecode_user = self.request.user
219
216
220 if include_app_defaults:
217 if include_app_defaults:
221 from rhodecode.lib.base import attach_context_attributes
218 from rhodecode.lib.base import attach_context_attributes
222
219
223 attach_context_attributes(c, self.request, self.request.user.user_id)
220 attach_context_attributes(c, self.request, self.request.user.user_id)
224
221
225 c.is_super_admin = c.auth_user.is_admin
222 c.is_super_admin = c.auth_user.is_admin
226
223
227 c.can_create_repo = c.is_super_admin
224 c.can_create_repo = c.is_super_admin
228 c.can_create_repo_group = c.is_super_admin
225 c.can_create_repo_group = c.is_super_admin
229 c.can_create_user_group = c.is_super_admin
226 c.can_create_user_group = c.is_super_admin
230
227
231 c.is_delegated_admin = False
228 c.is_delegated_admin = False
232
229
233 if not c.auth_user.is_default and not c.is_super_admin:
230 if not c.auth_user.is_default and not c.is_super_admin:
234 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
231 c.can_create_repo = h.HasPermissionAny("hg.create.repository")(
235 user=self.request.user
232 user=self.request.user
236 )
233 )
237 repositories = c.auth_user.repositories_admin or c.can_create_repo
234 repositories = c.auth_user.repositories_admin or c.can_create_repo
238
235
239 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
236 c.can_create_repo_group = h.HasPermissionAny("hg.repogroup.create.true")(
240 user=self.request.user
237 user=self.request.user
241 )
238 )
242 repository_groups = (
239 repository_groups = (
243 c.auth_user.repository_groups_admin or c.can_create_repo_group
240 c.auth_user.repository_groups_admin or c.can_create_repo_group
244 )
241 )
245
242
246 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
243 c.can_create_user_group = h.HasPermissionAny("hg.usergroup.create.true")(
247 user=self.request.user
244 user=self.request.user
248 )
245 )
249 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
246 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
250 # delegated admin can create, or manage some objects
247 # delegated admin can create, or manage some objects
251 c.is_delegated_admin = repositories or repository_groups or user_groups
248 c.is_delegated_admin = repositories or repository_groups or user_groups
252 return c
249 return c
253
250
254 def _get_template_context(self, tmpl_args, **kwargs):
251 def _get_template_context(self, tmpl_args, **kwargs):
255 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
252 local_tmpl_args = {"defaults": {}, "errors": {}, "c": tmpl_args}
256 local_tmpl_args.update(kwargs)
253 local_tmpl_args.update(kwargs)
257 return local_tmpl_args
254 return local_tmpl_args
258
255
259 def load_default_context(self):
256 def load_default_context(self):
260 """
257 """
261 example:
258 example:
262
259
263 def load_default_context(self):
260 def load_default_context(self):
264 c = self._get_local_tmpl_context()
261 c = self._get_local_tmpl_context()
265 c.custom_var = 'foobar'
262 c.custom_var = 'foobar'
266
263
267 return c
264 return c
268 """
265 """
269 raise NotImplementedError("Needs implementation in view class")
266 raise NotImplementedError("Needs implementation in view class")
270
267
271
268
272 class RepoAppView(BaseAppView):
269 class RepoAppView(BaseAppView):
273 def __init__(self, context, request):
270 def __init__(self, context, request):
274 super().__init__(context, request)
271 super().__init__(context, request)
275 self.db_repo = request.db_repo
272 self.db_repo = request.db_repo
276 self.db_repo_name = self.db_repo.repo_name
273 self.db_repo_name = self.db_repo.repo_name
277 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
274 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
278 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
275 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
279 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
276 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
280
277
281 def _handle_missing_requirements(self, error):
278 def _handle_missing_requirements(self, error):
282 log.error(
279 log.error(
283 "Requirements are missing for repository %s: %s",
280 "Requirements are missing for repository %s: %s",
284 self.db_repo_name,
281 self.db_repo_name,
285 safe_str(error),
282 safe_str(error),
286 )
283 )
287
284
288 def _prepare_and_set_clone_url(self, c):
285 def _prepare_and_set_clone_url(self, c):
289 username = ""
286 username = ""
290 if self._rhodecode_user.username != User.DEFAULT_USER:
287 if self._rhodecode_user.username != User.DEFAULT_USER:
291 username = self._rhodecode_user.username
288 username = self._rhodecode_user.username
292
289
293 _def_clone_uri = c.clone_uri_tmpl
290 _def_clone_uri = c.clone_uri_tmpl
294 _def_clone_uri_id = c.clone_uri_id_tmpl
291 _def_clone_uri_id = c.clone_uri_id_tmpl
295 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
292 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
296
293
297 c.clone_repo_url = self.db_repo.clone_url(
294 c.clone_repo_url = self.db_repo.clone_url(
298 user=username, uri_tmpl=_def_clone_uri
295 user=username, uri_tmpl=_def_clone_uri
299 )
296 )
300 c.clone_repo_url_id = self.db_repo.clone_url(
297 c.clone_repo_url_id = self.db_repo.clone_url(
301 user=username, uri_tmpl=_def_clone_uri_id
298 user=username, uri_tmpl=_def_clone_uri_id
302 )
299 )
303 c.clone_repo_url_ssh = self.db_repo.clone_url(
300 c.clone_repo_url_ssh = self.db_repo.clone_url(
304 uri_tmpl=_def_clone_uri_ssh, ssh=True
301 uri_tmpl=_def_clone_uri_ssh, ssh=True
305 )
302 )
306
303
307 def _get_local_tmpl_context(self, include_app_defaults=True):
304 def _get_local_tmpl_context(self, include_app_defaults=True):
308 _ = self.request.translate
305 _ = self.request.translate
309 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
306 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
310
307
311 # register common vars for this type of view
308 # register common vars for this type of view
312 c.rhodecode_db_repo = self.db_repo
309 c.rhodecode_db_repo = self.db_repo
313 c.repo_name = self.db_repo_name
310 c.repo_name = self.db_repo_name
314 c.repository_pull_requests = self.db_repo_pull_requests
311 c.repository_pull_requests = self.db_repo_pull_requests
315 c.repository_artifacts = self.db_repo_artifacts
312 c.repository_artifacts = self.db_repo_artifacts
316 c.repository_is_user_following = ScmModel().is_following_repo(
313 c.repository_is_user_following = ScmModel().is_following_repo(
317 self.db_repo_name, self._rhodecode_user.user_id
314 self.db_repo_name, self._rhodecode_user.user_id
318 )
315 )
319 self.path_filter = PathFilter(None)
316 self.path_filter = PathFilter(None)
320
317
321 c.repository_requirements_missing = {}
318 c.repository_requirements_missing = {}
322 try:
319 try:
323 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
320 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
324 # NOTE(marcink):
321 # NOTE(marcink):
325 # comparison to None since if it's an object __bool__ is expensive to
322 # comparison to None since if it's an object __bool__ is expensive to
326 # calculate
323 # calculate
327 if self.rhodecode_vcs_repo is not None:
324 if self.rhodecode_vcs_repo is not None:
328 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
325 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
329 c.auth_user.username
326 c.auth_user.username
330 )
327 )
331 self.path_filter = PathFilter(path_perms)
328 self.path_filter = PathFilter(path_perms)
332 except RepositoryRequirementError as e:
329 except RepositoryRequirementError as e:
333 c.repository_requirements_missing = {"error": str(e)}
330 c.repository_requirements_missing = {"error": str(e)}
334 self._handle_missing_requirements(e)
331 self._handle_missing_requirements(e)
335 self.rhodecode_vcs_repo = None
332 self.rhodecode_vcs_repo = None
336
333
337 c.path_filter = self.path_filter # used by atom_feed_entry.mako
334 c.path_filter = self.path_filter # used by atom_feed_entry.mako
338
335
339 if self.rhodecode_vcs_repo is None:
336 if self.rhodecode_vcs_repo is None:
340 # unable to fetch this repo as vcs instance, report back to user
337 # unable to fetch this repo as vcs instance, report back to user
341 log.debug(
338 log.debug(
342 "Repository was not found on filesystem, check if it exists or is not damaged"
339 "Repository was not found on filesystem, check if it exists or is not damaged"
343 )
340 )
344 h.flash(
341 h.flash(
345 _(
342 _(
346 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
343 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
347 "Please check if it exist, or is not damaged."
344 "Please check if it exist, or is not damaged."
348 )
345 )
349 % {"repo_name": c.repo_name},
346 % {"repo_name": c.repo_name},
350 category="error",
347 category="error",
351 ignore_duplicate=True,
348 ignore_duplicate=True,
352 )
349 )
353 if c.repository_requirements_missing:
350 if c.repository_requirements_missing:
354 route = self.request.matched_route.name
351 route = self.request.matched_route.name
355 if route.startswith(("edit_repo", "repo_summary")):
352 if route.startswith(("edit_repo", "repo_summary")):
356 # allow summary and edit repo on missing requirements
353 # allow summary and edit repo on missing requirements
357 return c
354 return c
358
355
359 raise HTTPFound(
356 raise HTTPFound(
360 h.route_path("repo_summary", repo_name=self.db_repo_name)
357 h.route_path("repo_summary", repo_name=self.db_repo_name)
361 )
358 )
362
359
363 else: # redirect if we don't show missing requirements
360 else: # redirect if we don't show missing requirements
364 raise HTTPFound(h.route_path("home"))
361 raise HTTPFound(h.route_path("home"))
365
362
366 c.has_origin_repo_read_perm = False
363 c.has_origin_repo_read_perm = False
367 if self.db_repo.fork:
364 if self.db_repo.fork:
368 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
365 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
369 "repository.write", "repository.read", "repository.admin"
366 "repository.write", "repository.read", "repository.admin"
370 )(self.db_repo.fork.repo_name, "summary fork link")
367 )(self.db_repo.fork.repo_name, "summary fork link")
371
368
372 return c
369 return c
373
370
374 def _get_f_path_unchecked(self, matchdict, default=None):
371 def _get_f_path_unchecked(self, matchdict, default=None):
375 """
372 """
376 Should only be used by redirects, everything else should call _get_f_path
373 Should only be used by redirects, everything else should call _get_f_path
377 """
374 """
378 f_path = matchdict.get("f_path")
375 f_path = matchdict.get("f_path")
379 if f_path:
376 if f_path:
380 # fix for multiple initial slashes that causes errors for GIT
377 # fix for multiple initial slashes that causes errors for GIT
381 return f_path.lstrip("/")
378 return f_path.lstrip("/")
382
379
383 return default
380 return default
384
381
385 def _get_f_path(self, matchdict, default=None):
382 def _get_f_path(self, matchdict, default=None):
386 f_path_match = self._get_f_path_unchecked(matchdict, default)
383 f_path_match = self._get_f_path_unchecked(matchdict, default)
387 return self.path_filter.assert_path_permissions(f_path_match)
384 return self.path_filter.assert_path_permissions(f_path_match)
388
385
389 def _get_general_setting(self, target_repo, settings_key, default=False):
386 def _get_general_setting(self, target_repo, settings_key, default=False):
390 settings_model = VcsSettingsModel(repo=target_repo)
387 settings_model = VcsSettingsModel(repo=target_repo)
391 settings = settings_model.get_general_settings()
388 settings = settings_model.get_general_settings()
392 return settings.get(settings_key, default)
389 return settings.get(settings_key, default)
393
390
394 def _get_repo_setting(self, target_repo, settings_key, default=False):
391 def _get_repo_setting(self, target_repo, settings_key, default=False):
395 settings_model = VcsSettingsModel(repo=target_repo)
392 settings_model = VcsSettingsModel(repo=target_repo)
396 settings = settings_model.get_repo_settings_inherited()
393 settings = settings_model.get_repo_settings_inherited()
397 return settings.get(settings_key, default)
394 return settings.get(settings_key, default)
398
395
399 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
396 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path="/"):
400 log.debug("Looking for README file at path %s", path)
397 log.debug("Looking for README file at path %s", path)
401 if commit_id:
398 if commit_id:
402 landing_commit_id = commit_id
399 landing_commit_id = commit_id
403 else:
400 else:
404 landing_commit = db_repo.get_landing_commit()
401 landing_commit = db_repo.get_landing_commit()
405 if isinstance(landing_commit, EmptyCommit):
402 if isinstance(landing_commit, EmptyCommit):
406 return None, None
403 return None, None
407 landing_commit_id = landing_commit.raw_id
404 landing_commit_id = landing_commit.raw_id
408
405
409 cache_namespace_uid = f"repo.{db_repo.repo_id}"
406 cache_namespace_uid = f"repo.{db_repo.repo_id}"
410 region = rc_cache.get_or_create_region(
407 region = rc_cache.get_or_create_region(
411 "cache_repo", cache_namespace_uid, use_async_runner=False
408 "cache_repo", cache_namespace_uid, use_async_runner=False
412 )
409 )
413 start = time.time()
410 start = time.time()
414
411
415 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
412 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
416 def generate_repo_readme(
413 def generate_repo_readme(
417 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
414 repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type
418 ):
415 ):
419 readme_data = None
416 readme_data = None
420 readme_filename = None
417 readme_filename = None
421
418
422 commit = db_repo.get_commit(_commit_id)
419 commit = db_repo.get_commit(_commit_id)
423 log.debug("Searching for a README file at commit %s.", _commit_id)
420 log.debug("Searching for a README file at commit %s.", _commit_id)
424 readme_node = ReadmeFinder(_renderer_type).search(
421 readme_node = ReadmeFinder(_renderer_type).search(
425 commit, path=_readme_search_path
422 commit, path=_readme_search_path
426 )
423 )
427
424
428 if readme_node:
425 if readme_node:
429 log.debug("Found README node: %s", readme_node)
426 log.debug("Found README node: %s", readme_node)
430
427
431 relative_urls = {
428 relative_urls = {
432 "raw": h.route_path(
429 "raw": h.route_path(
433 "repo_file_raw",
430 "repo_file_raw",
434 repo_name=_repo_name,
431 repo_name=_repo_name,
435 commit_id=commit.raw_id,
432 commit_id=commit.raw_id,
436 f_path=readme_node.path,
433 f_path=readme_node.path,
437 ),
434 ),
438 "standard": h.route_path(
435 "standard": h.route_path(
439 "repo_files",
436 "repo_files",
440 repo_name=_repo_name,
437 repo_name=_repo_name,
441 commit_id=commit.raw_id,
438 commit_id=commit.raw_id,
442 f_path=readme_node.path,
439 f_path=readme_node.path,
443 ),
440 ),
444 }
441 }
445
442
446 readme_data = self._render_readme_or_none(
443 readme_data = self._render_readme_or_none(
447 commit, readme_node, relative_urls
444 commit, readme_node, relative_urls
448 )
445 )
449 readme_filename = readme_node.str_path
446 readme_filename = readme_node.str_path
450
447
451 return readme_data, readme_filename
448 return readme_data, readme_filename
452
449
453 readme_data, readme_filename = generate_repo_readme(
450 readme_data, readme_filename = generate_repo_readme(
454 db_repo.repo_id,
451 db_repo.repo_id,
455 landing_commit_id,
452 landing_commit_id,
456 db_repo.repo_name,
453 db_repo.repo_name,
457 path,
454 path,
458 renderer_type,
455 renderer_type,
459 )
456 )
460
457
461 compute_time = time.time() - start
458 compute_time = time.time() - start
462 log.debug(
459 log.debug(
463 "Repo README for path %s generated and computed in %.4fs",
460 "Repo README for path %s generated and computed in %.4fs",
464 path,
461 path,
465 compute_time,
462 compute_time,
466 )
463 )
467 return readme_data, readme_filename
464 return readme_data, readme_filename
468
465
469 def _render_readme_or_none(self, commit, readme_node, relative_urls):
466 def _render_readme_or_none(self, commit, readme_node, relative_urls):
470 log.debug("Found README file `%s` rendering...", readme_node.path)
467 log.debug("Found README file `%s` rendering...", readme_node.path)
471 renderer = MarkupRenderer()
468 renderer = MarkupRenderer()
472 try:
469 try:
473 html_source = renderer.render(
470 html_source = renderer.render(
474 readme_node.str_content, filename=readme_node.path
471 readme_node.str_content, filename=readme_node.path
475 )
472 )
476 if relative_urls:
473 if relative_urls:
477 return relative_links(html_source, relative_urls)
474 return relative_links(html_source, relative_urls)
478 return html_source
475 return html_source
479 except Exception:
476 except Exception:
480 log.exception("Exception while trying to render the README")
477 log.exception("Exception while trying to render the README")
481
478
482 def get_recache_flag(self):
479 def get_recache_flag(self):
483 for flag_name in ["force_recache", "force-recache", "no-cache"]:
480 for flag_name in ["force_recache", "force-recache", "no-cache"]:
484 flag_val = self.request.GET.get(flag_name)
481 flag_val = self.request.GET.get(flag_name)
485 if str2bool(flag_val):
482 if str2bool(flag_val):
486 return True
483 return True
487 return False
484 return False
488
485
489 def get_commit_preload_attrs(cls):
486 def get_commit_preload_attrs(cls):
490 pre_load = [
487 pre_load = [
491 "author",
488 "author",
492 "branch",
489 "branch",
493 "date",
490 "date",
494 "message",
491 "message",
495 "parents",
492 "parents",
496 "obsolete",
493 "obsolete",
497 "phase",
494 "phase",
498 "hidden",
495 "hidden",
499 ]
496 ]
500 return pre_load
497 return pre_load
501
498
502
499
503 class PathFilter(object):
500 class PathFilter(object):
504 # Expects and instance of BasePathPermissionChecker or None
501 # Expects and instance of BasePathPermissionChecker or None
505 def __init__(self, permission_checker):
502 def __init__(self, permission_checker):
506 self.permission_checker = permission_checker
503 self.permission_checker = permission_checker
507
504
508 def assert_path_permissions(self, path):
505 def assert_path_permissions(self, path):
509 if self.path_access_allowed(path):
506 if self.path_access_allowed(path):
510 return path
507 return path
511 raise HTTPForbidden()
508 raise HTTPForbidden()
512
509
513 def path_access_allowed(self, path):
510 def path_access_allowed(self, path):
514 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
511 log.debug("Checking ACL permissions for PathFilter for `%s`", path)
515 if self.permission_checker:
512 if self.permission_checker:
516 has_access = path and self.permission_checker.has_access(path)
513 has_access = path and self.permission_checker.has_access(path)
517 log.debug(
514 log.debug(
518 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
515 "ACL Permissions checker enabled, ACL Check has_access: %s", has_access
519 )
516 )
520 return has_access
517 return has_access
521
518
522 log.debug("ACL permissions checker not enabled, skipping...")
519 log.debug("ACL permissions checker not enabled, skipping...")
523 return True
520 return True
524
521
525 def filter_patchset(self, patchset):
522 def filter_patchset(self, patchset):
526 if not self.permission_checker or not patchset:
523 if not self.permission_checker or not patchset:
527 return patchset, False
524 return patchset, False
528 had_filtered = False
525 had_filtered = False
529 filtered_patchset = []
526 filtered_patchset = []
530 for patch in patchset:
527 for patch in patchset:
531 filename = patch.get("filename", None)
528 filename = patch.get("filename", None)
532 if not filename or self.permission_checker.has_access(filename):
529 if not filename or self.permission_checker.has_access(filename):
533 filtered_patchset.append(patch)
530 filtered_patchset.append(patch)
534 else:
531 else:
535 had_filtered = True
532 had_filtered = True
536 if had_filtered:
533 if had_filtered:
537 if isinstance(patchset, diffs.LimitedDiffContainer):
534 if isinstance(patchset, diffs.LimitedDiffContainer):
538 filtered_patchset = diffs.LimitedDiffContainer(
535 filtered_patchset = diffs.LimitedDiffContainer(
539 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
536 patchset.diff_limit, patchset.cur_diff_size, filtered_patchset
540 )
537 )
541 return filtered_patchset, True
538 return filtered_patchset, True
542 else:
539 else:
543 return patchset, False
540 return patchset, False
544
541
545 def render_patchset_filtered(
542 def render_patchset_filtered(
546 self, diffset, patchset, source_ref=None, target_ref=None
543 self, diffset, patchset, source_ref=None, target_ref=None
547 ):
544 ):
548 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
545 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
549 result = diffset.render_patchset(
546 result = diffset.render_patchset(
550 filtered_patchset, source_ref=source_ref, target_ref=target_ref
547 filtered_patchset, source_ref=source_ref, target_ref=target_ref
551 )
548 )
552 result.has_hidden_changes = has_hidden_changes
549 result.has_hidden_changes = has_hidden_changes
553 return result
550 return result
554
551
555 def get_raw_patch(self, diff_processor):
552 def get_raw_patch(self, diff_processor):
556 if self.permission_checker is None:
553 if self.permission_checker is None:
557 return diff_processor.as_raw()
554 return diff_processor.as_raw()
558 elif self.permission_checker.has_full_access:
555 elif self.permission_checker.has_full_access:
559 return diff_processor.as_raw()
556 return diff_processor.as_raw()
560 else:
557 else:
561 return "# Repository has user-specific filters, raw patch generation is disabled."
558 return "# Repository has user-specific filters, raw patch generation is disabled."
562
559
563 @property
560 @property
564 def is_enabled(self):
561 def is_enabled(self):
565 return self.permission_checker is not None
562 return self.permission_checker is not None
566
563
567
564
568 class RepoGroupAppView(BaseAppView):
565 class RepoGroupAppView(BaseAppView):
569 def __init__(self, context, request):
566 def __init__(self, context, request):
570 super().__init__(context, request)
567 super().__init__(context, request)
571 self.db_repo_group = request.db_repo_group
568 self.db_repo_group = request.db_repo_group
572 self.db_repo_group_name = self.db_repo_group.group_name
569 self.db_repo_group_name = self.db_repo_group.group_name
573
570
574 def _get_local_tmpl_context(self, include_app_defaults=True):
571 def _get_local_tmpl_context(self, include_app_defaults=True):
575 _ = self.request.translate
572 _ = self.request.translate
576 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
573 c = super()._get_local_tmpl_context(include_app_defaults=include_app_defaults)
577 c.repo_group = self.db_repo_group
574 c.repo_group = self.db_repo_group
578 return c
575 return c
579
576
580 def _revoke_perms_on_yourself(self, form_result):
577 def _revoke_perms_on_yourself(self, form_result):
581 _updates = [
578 _updates = [
582 u
579 u
583 for u in form_result["perm_updates"]
580 for u in form_result["perm_updates"]
584 if self._rhodecode_user.user_id == int(u[0])
581 if self._rhodecode_user.user_id == int(u[0])
585 ]
582 ]
586 _additions = [
583 _additions = [
587 u
584 u
588 for u in form_result["perm_additions"]
585 for u in form_result["perm_additions"]
589 if self._rhodecode_user.user_id == int(u[0])
586 if self._rhodecode_user.user_id == int(u[0])
590 ]
587 ]
591 _deletions = [
588 _deletions = [
592 u
589 u
593 for u in form_result["perm_deletions"]
590 for u in form_result["perm_deletions"]
594 if self._rhodecode_user.user_id == int(u[0])
591 if self._rhodecode_user.user_id == int(u[0])
595 ]
592 ]
596 admin_perm = "group.admin"
593 admin_perm = "group.admin"
597 if (
594 if (
598 _updates
595 _updates
599 and _updates[0][1] != admin_perm
596 and _updates[0][1] != admin_perm
600 or _additions
597 or _additions
601 and _additions[0][1] != admin_perm
598 and _additions[0][1] != admin_perm
602 or _deletions
599 or _deletions
603 and _deletions[0][1] != admin_perm
600 and _deletions[0][1] != admin_perm
604 ):
601 ):
605 return True
602 return True
606 return False
603 return False
607
604
608
605
609 class UserGroupAppView(BaseAppView):
606 class UserGroupAppView(BaseAppView):
610 def __init__(self, context, request):
607 def __init__(self, context, request):
611 super().__init__(context, request)
608 super().__init__(context, request)
612 self.db_user_group = request.db_user_group
609 self.db_user_group = request.db_user_group
613 self.db_user_group_name = self.db_user_group.users_group_name
610 self.db_user_group_name = self.db_user_group.users_group_name
614
611
615
612
616 class UserAppView(BaseAppView):
613 class UserAppView(BaseAppView):
617 def __init__(self, context, request):
614 def __init__(self, context, request):
618 super().__init__(context, request)
615 super().__init__(context, request)
619 self.db_user = request.db_user
616 self.db_user = request.db_user
620 self.db_user_id = self.db_user.user_id
617 self.db_user_id = self.db_user.user_id
621
618
622 _ = self.request.translate
619 _ = self.request.translate
623 if not request.db_user_supports_default:
620 if not request.db_user_supports_default:
624 if self.db_user.username == User.DEFAULT_USER:
621 if self.db_user.username == User.DEFAULT_USER:
625 h.flash(
622 h.flash(
626 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
623 _("Editing user `{}` is disabled.".format(User.DEFAULT_USER)),
627 category="warning",
624 category="warning",
628 )
625 )
629 raise HTTPFound(h.route_path("users"))
626 raise HTTPFound(h.route_path("users"))
630
627
631
628
632 class DataGridAppView(object):
629 class DataGridAppView(object):
633 """
630 """
634 Common class to have re-usable grid rendering components
631 Common class to have re-usable grid rendering components
635 """
632 """
636
633
637 def _extract_ordering(self, request, column_map=None):
634 def _extract_ordering(self, request, column_map=None):
638 column_map = column_map or {}
635 column_map = column_map or {}
639 column_index = safe_int(request.GET.get("order[0][column]"))
636 column_index = safe_int(request.GET.get("order[0][column]"))
640 order_dir = request.GET.get("order[0][dir]", "desc")
637 order_dir = request.GET.get("order[0][dir]", "desc")
641 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
638 order_by = request.GET.get("columns[%s][data][sort]" % column_index, "name_raw")
642
639
643 # translate datatable to DB columns
640 # translate datatable to DB columns
644 order_by = column_map.get(order_by) or order_by
641 order_by = column_map.get(order_by) or order_by
645
642
646 search_q = request.GET.get("search[value]")
643 search_q = request.GET.get("search[value]")
647 return search_q, order_by, order_dir
644 return search_q, order_by, order_dir
648
645
649 def _extract_chunk(self, request):
646 def _extract_chunk(self, request):
650 start = safe_int(request.GET.get("start"), 0)
647 start = safe_int(request.GET.get("start"), 0)
651 length = safe_int(request.GET.get("length"), 25)
648 length = safe_int(request.GET.get("length"), 25)
652 draw = safe_int(request.GET.get("draw"))
649 draw = safe_int(request.GET.get("draw"))
653 return draw, start, length
650 return draw, start, length
654
651
655 def _get_order_col(self, order_by, model):
652 def _get_order_col(self, order_by, model):
656 if isinstance(order_by, str):
653 if isinstance(order_by, str):
657 try:
654 try:
658 return operator.attrgetter(order_by)(model)
655 return operator.attrgetter(order_by)(model)
659 except AttributeError:
656 except AttributeError:
660 return None
657 return None
661 else:
658 else:
662 return order_by
659 return order_by
663
660
664
661
665 class BaseReferencesView(RepoAppView):
662 class BaseReferencesView(RepoAppView):
666 """
663 """
667 Base for reference view for branches, tags and bookmarks.
664 Base for reference view for branches, tags and bookmarks.
668 """
665 """
669
666
670 def load_default_context(self):
667 def load_default_context(self):
671 c = self._get_local_tmpl_context()
668 c = self._get_local_tmpl_context()
672 return c
669 return c
673
670
674 def load_refs_context(self, ref_items, partials_template):
671 def load_refs_context(self, ref_items, partials_template):
675 _render = self.request.get_partial_renderer(partials_template)
672 _render = self.request.get_partial_renderer(partials_template)
676 pre_load = ["author", "date", "message", "parents"]
673 pre_load = ["author", "date", "message", "parents"]
677
674
678 is_svn = h.is_svn(self.rhodecode_vcs_repo)
675 is_svn = h.is_svn(self.rhodecode_vcs_repo)
679 is_hg = h.is_hg(self.rhodecode_vcs_repo)
676 is_hg = h.is_hg(self.rhodecode_vcs_repo)
680
677
681 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
678 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
682
679
683 closed_refs = {}
680 closed_refs = {}
684 if is_hg:
681 if is_hg:
685 closed_refs = self.rhodecode_vcs_repo.branches_closed
682 closed_refs = self.rhodecode_vcs_repo.branches_closed
686
683
687 data = []
684 data = []
688 for ref_name, commit_id in ref_items:
685 for ref_name, commit_id in ref_items:
689 commit = self.rhodecode_vcs_repo.get_commit(
686 commit = self.rhodecode_vcs_repo.get_commit(
690 commit_id=commit_id, pre_load=pre_load
687 commit_id=commit_id, pre_load=pre_load
691 )
688 )
692 closed = ref_name in closed_refs
689 closed = ref_name in closed_refs
693
690
694 # TODO: johbo: Unify generation of reference links
691 # TODO: johbo: Unify generation of reference links
695 use_commit_id = "/" in ref_name or is_svn
692 use_commit_id = "/" in ref_name or is_svn
696
693
697 if use_commit_id:
694 if use_commit_id:
698 files_url = h.route_path(
695 files_url = h.route_path(
699 "repo_files",
696 "repo_files",
700 repo_name=self.db_repo_name,
697 repo_name=self.db_repo_name,
701 f_path=ref_name if is_svn else "",
698 f_path=ref_name if is_svn else "",
702 commit_id=commit_id,
699 commit_id=commit_id,
703 _query=dict(at=ref_name),
700 _query=dict(at=ref_name),
704 )
701 )
705
702
706 else:
703 else:
707 files_url = h.route_path(
704 files_url = h.route_path(
708 "repo_files",
705 "repo_files",
709 repo_name=self.db_repo_name,
706 repo_name=self.db_repo_name,
710 f_path=ref_name if is_svn else "",
707 f_path=ref_name if is_svn else "",
711 commit_id=ref_name,
708 commit_id=ref_name,
712 _query=dict(at=ref_name),
709 _query=dict(at=ref_name),
713 )
710 )
714
711
715 data.append(
712 data.append(
716 {
713 {
717 "name": _render("name", ref_name, files_url, closed),
714 "name": _render("name", ref_name, files_url, closed),
718 "name_raw": ref_name,
715 "name_raw": ref_name,
719 "date": _render("date", commit.date),
716 "date": _render("date", commit.date),
720 "date_raw": datetime_to_time(commit.date),
717 "date_raw": datetime_to_time(commit.date),
721 "author": _render("author", commit.author),
718 "author": _render("author", commit.author),
722 "commit": _render(
719 "commit": _render(
723 "commit", commit.message, commit.raw_id, commit.idx
720 "commit", commit.message, commit.raw_id, commit.idx
724 ),
721 ),
725 "commit_raw": commit.idx,
722 "commit_raw": commit.idx,
726 "compare": _render(
723 "compare": _render(
727 "compare", format_ref_id(ref_name, commit.raw_id)
724 "compare", format_ref_id(ref_name, commit.raw_id)
728 ),
725 ),
729 }
726 }
730 )
727 )
731
728
732 return data
729 return data
733
730
734
731
735 class RepoRoutePredicate(object):
732 class RepoRoutePredicate(object):
736 def __init__(self, val, config):
733 def __init__(self, val, config):
737 self.val = val
734 self.val = val
738
735
739 def text(self):
736 def text(self):
740 return f"repo_route = {self.val}"
737 return f"repo_route = {self.val}"
741
738
742 phash = text
739 phash = text
743
740
744 def __call__(self, info, request):
741 def __call__(self, info, request):
745 if hasattr(request, "vcs_call"):
742 if hasattr(request, "vcs_call"):
746 # skip vcs calls
743 # skip vcs calls
747 return
744 return
748
745
749 repo_name = info["match"]["repo_name"]
746 repo_name = info["match"]["repo_name"]
750
747
751 repo_name_parts = repo_name.split("/")
748 repo_name_parts = repo_name.split("/")
752 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
749 repo_slugs = [x for x in (repo_name_slug(x) for x in repo_name_parts)]
753
750
754 if repo_name_parts != repo_slugs:
751 if repo_name_parts != repo_slugs:
755 # short-skip if the repo-name doesn't follow slug rule
752 # short-skip if the repo-name doesn't follow slug rule
756 log.warning(
753 log.warning(
757 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
754 "repo_name: %s is different than slug %s", repo_name_parts, repo_slugs
758 )
755 )
759 return False
756 return False
760
757
761 repo_model = repo.RepoModel()
758 repo_model = repo.RepoModel()
762
759
763 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
760 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
764
761
765 def redirect_if_creating(route_info, db_repo):
762 def redirect_if_creating(route_info, db_repo):
766 skip_views = ["edit_repo_advanced_delete"]
763 skip_views = ["edit_repo_advanced_delete"]
767 route = route_info["route"]
764 route = route_info["route"]
768 # we should skip delete view so we can actually "remove" repositories
765 # we should skip delete view so we can actually "remove" repositories
769 # if they get stuck in creating state.
766 # if they get stuck in creating state.
770 if route.name in skip_views:
767 if route.name in skip_views:
771 return
768 return
772
769
773 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
770 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
774 repo_creating_url = request.route_path(
771 repo_creating_url = request.route_path(
775 "repo_creating", repo_name=db_repo.repo_name
772 "repo_creating", repo_name=db_repo.repo_name
776 )
773 )
777 raise HTTPFound(repo_creating_url)
774 raise HTTPFound(repo_creating_url)
778
775
779 if by_name_match:
776 if by_name_match:
780 # register this as request object we can re-use later
777 # register this as request object we can re-use later
781 request.db_repo = by_name_match
778 request.db_repo = by_name_match
782 request.db_repo_name = request.db_repo.repo_name
779 request.db_repo_name = request.db_repo.repo_name
783
780
784 redirect_if_creating(info, by_name_match)
781 redirect_if_creating(info, by_name_match)
785 return True
782 return True
786
783
787 by_id_match = repo_model.get_repo_by_id(repo_name)
784 by_id_match = repo_model.get_repo_by_id(repo_name)
788 if by_id_match:
785 if by_id_match:
789 request.db_repo = by_id_match
786 request.db_repo = by_id_match
790 request.db_repo_name = request.db_repo.repo_name
787 request.db_repo_name = request.db_repo.repo_name
791 redirect_if_creating(info, by_id_match)
788 redirect_if_creating(info, by_id_match)
792 return True
789 return True
793
790
794 return False
791 return False
795
792
796
793
797 class RepoForbidArchivedRoutePredicate(object):
794 class RepoForbidArchivedRoutePredicate(object):
798 def __init__(self, val, config):
795 def __init__(self, val, config):
799 self.val = val
796 self.val = val
800
797
801 def text(self):
798 def text(self):
802 return f"repo_forbid_archived = {self.val}"
799 return f"repo_forbid_archived = {self.val}"
803
800
804 phash = text
801 phash = text
805
802
806 def __call__(self, info, request):
803 def __call__(self, info, request):
807 _ = request.translate
804 _ = request.translate
808 rhodecode_db_repo = request.db_repo
805 rhodecode_db_repo = request.db_repo
809
806
810 log.debug(
807 log.debug(
811 "%s checking if archived flag for repo for %s",
808 "%s checking if archived flag for repo for %s",
812 self.__class__.__name__,
809 self.__class__.__name__,
813 rhodecode_db_repo.repo_name,
810 rhodecode_db_repo.repo_name,
814 )
811 )
815
812
816 if rhodecode_db_repo.archived:
813 if rhodecode_db_repo.archived:
817 log.warning(
814 log.warning(
818 "Current view is not supported for archived repo:%s",
815 "Current view is not supported for archived repo:%s",
819 rhodecode_db_repo.repo_name,
816 rhodecode_db_repo.repo_name,
820 )
817 )
821
818
822 h.flash(
819 h.flash(
823 h.literal(_("Action not supported for archived repository.")),
820 h.literal(_("Action not supported for archived repository.")),
824 category="warning",
821 category="warning",
825 )
822 )
826 summary_url = request.route_path(
823 summary_url = request.route_path(
827 "repo_summary", repo_name=rhodecode_db_repo.repo_name
824 "repo_summary", repo_name=rhodecode_db_repo.repo_name
828 )
825 )
829 raise HTTPFound(summary_url)
826 raise HTTPFound(summary_url)
830 return True
827 return True
831
828
832
829
833 class RepoTypeRoutePredicate(object):
830 class RepoTypeRoutePredicate(object):
834 def __init__(self, val, config):
831 def __init__(self, val, config):
835 self.val = val or ["hg", "git", "svn"]
832 self.val = val or ["hg", "git", "svn"]
836
833
837 def text(self):
834 def text(self):
838 return f"repo_accepted_type = {self.val}"
835 return f"repo_accepted_type = {self.val}"
839
836
840 phash = text
837 phash = text
841
838
842 def __call__(self, info, request):
839 def __call__(self, info, request):
843 if hasattr(request, "vcs_call"):
840 if hasattr(request, "vcs_call"):
844 # skip vcs calls
841 # skip vcs calls
845 return
842 return
846
843
847 rhodecode_db_repo = request.db_repo
844 rhodecode_db_repo = request.db_repo
848
845
849 log.debug(
846 log.debug(
850 "%s checking repo type for %s in %s",
847 "%s checking repo type for %s in %s",
851 self.__class__.__name__,
848 self.__class__.__name__,
852 rhodecode_db_repo.repo_type,
849 rhodecode_db_repo.repo_type,
853 self.val,
850 self.val,
854 )
851 )
855
852
856 if rhodecode_db_repo.repo_type in self.val:
853 if rhodecode_db_repo.repo_type in self.val:
857 return True
854 return True
858 else:
855 else:
859 log.warning(
856 log.warning(
860 "Current view is not supported for repo type:%s",
857 "Current view is not supported for repo type:%s",
861 rhodecode_db_repo.repo_type,
858 rhodecode_db_repo.repo_type,
862 )
859 )
863 return False
860 return False
864
861
865
862
866 class RepoGroupRoutePredicate(object):
863 class RepoGroupRoutePredicate(object):
867 def __init__(self, val, config):
864 def __init__(self, val, config):
868 self.val = val
865 self.val = val
869
866
870 def text(self):
867 def text(self):
871 return f"repo_group_route = {self.val}"
868 return f"repo_group_route = {self.val}"
872
869
873 phash = text
870 phash = text
874
871
875 def __call__(self, info, request):
872 def __call__(self, info, request):
876 if hasattr(request, "vcs_call"):
873 if hasattr(request, "vcs_call"):
877 # skip vcs calls
874 # skip vcs calls
878 return
875 return
879
876
880 repo_group_name = info["match"]["repo_group_name"]
877 repo_group_name = info["match"]["repo_group_name"]
881
878
882 repo_group_name_parts = repo_group_name.split("/")
879 repo_group_name_parts = repo_group_name.split("/")
883 repo_group_slugs = [
880 repo_group_slugs = [
884 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
881 x for x in [repo_name_slug(x) for x in repo_group_name_parts]
885 ]
882 ]
886 if repo_group_name_parts != repo_group_slugs:
883 if repo_group_name_parts != repo_group_slugs:
887 # short-skip if the repo-name doesn't follow slug rule
884 # short-skip if the repo-name doesn't follow slug rule
888 log.warning(
885 log.warning(
889 "repo_group_name: %s is different than slug %s",
886 "repo_group_name: %s is different than slug %s",
890 repo_group_name_parts,
887 repo_group_name_parts,
891 repo_group_slugs,
888 repo_group_slugs,
892 )
889 )
893 return False
890 return False
894
891
895 repo_group_model = repo_group.RepoGroupModel()
892 repo_group_model = repo_group.RepoGroupModel()
896 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
893 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
897
894
898 if by_name_match:
895 if by_name_match:
899 # register this as request object we can re-use later
896 # register this as request object we can re-use later
900 request.db_repo_group = by_name_match
897 request.db_repo_group = by_name_match
901 request.db_repo_group_name = request.db_repo_group.group_name
898 request.db_repo_group_name = request.db_repo_group.group_name
902 return True
899 return True
903
900
904 return False
901 return False
905
902
906
903
907 class UserGroupRoutePredicate(object):
904 class UserGroupRoutePredicate(object):
908 def __init__(self, val, config):
905 def __init__(self, val, config):
909 self.val = val
906 self.val = val
910
907
911 def text(self):
908 def text(self):
912 return f"user_group_route = {self.val}"
909 return f"user_group_route = {self.val}"
913
910
914 phash = text
911 phash = text
915
912
916 def __call__(self, info, request):
913 def __call__(self, info, request):
917 if hasattr(request, "vcs_call"):
914 if hasattr(request, "vcs_call"):
918 # skip vcs calls
915 # skip vcs calls
919 return
916 return
920
917
921 user_group_id = info["match"]["user_group_id"]
918 user_group_id = info["match"]["user_group_id"]
922 user_group_model = user_group.UserGroup()
919 user_group_model = user_group.UserGroup()
923 by_id_match = user_group_model.get(user_group_id, cache=False)
920 by_id_match = user_group_model.get(user_group_id, cache=False)
924
921
925 if by_id_match:
922 if by_id_match:
926 # register this as request object we can re-use later
923 # register this as request object we can re-use later
927 request.db_user_group = by_id_match
924 request.db_user_group = by_id_match
928 return True
925 return True
929
926
930 return False
927 return False
931
928
932
929
933 class UserRoutePredicateBase(object):
930 class UserRoutePredicateBase(object):
934 supports_default = None
931 supports_default = None
935
932
936 def __init__(self, val, config):
933 def __init__(self, val, config):
937 self.val = val
934 self.val = val
938
935
939 def text(self):
936 def text(self):
940 raise NotImplementedError()
937 raise NotImplementedError()
941
938
942 def __call__(self, info, request):
939 def __call__(self, info, request):
943 if hasattr(request, "vcs_call"):
940 if hasattr(request, "vcs_call"):
944 # skip vcs calls
941 # skip vcs calls
945 return
942 return
946
943
947 user_id = info["match"]["user_id"]
944 user_id = info["match"]["user_id"]
948 user_model = user.User()
945 user_model = user.User()
949 by_id_match = user_model.get(user_id, cache=False)
946 by_id_match = user_model.get(user_id, cache=False)
950
947
951 if by_id_match:
948 if by_id_match:
952 # register this as request object we can re-use later
949 # register this as request object we can re-use later
953 request.db_user = by_id_match
950 request.db_user = by_id_match
954 request.db_user_supports_default = self.supports_default
951 request.db_user_supports_default = self.supports_default
955 return True
952 return True
956
953
957 return False
954 return False
958
955
959
956
960 class UserRoutePredicate(UserRoutePredicateBase):
957 class UserRoutePredicate(UserRoutePredicateBase):
961 supports_default = False
958 supports_default = False
962
959
963 def text(self):
960 def text(self):
964 return f"user_route = {self.val}"
961 return f"user_route = {self.val}"
965
962
966 phash = text
963 phash = text
967
964
968
965
969 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
966 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
970 supports_default = True
967 supports_default = True
971
968
972 def text(self):
969 def text(self):
973 return f"user_with_default_route = {self.val}"
970 return f"user_with_default_route = {self.val}"
974
971
975 phash = text
972 phash = text
976
973
977
974
978 def includeme(config):
975 def includeme(config):
979 config.add_route_predicate("repo_route", RepoRoutePredicate)
976 config.add_route_predicate("repo_route", RepoRoutePredicate)
980 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
977 config.add_route_predicate("repo_accepted_types", RepoTypeRoutePredicate)
981 config.add_route_predicate(
978 config.add_route_predicate(
982 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
979 "repo_forbid_when_archived", RepoForbidArchivedRoutePredicate
983 )
980 )
984 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
981 config.add_route_predicate("repo_group_route", RepoGroupRoutePredicate)
985 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
982 config.add_route_predicate("user_group_route", UserGroupRoutePredicate)
986 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
983 config.add_route_predicate("user_route_with_default", UserRouteWithDefaultPredicate)
987 config.add_route_predicate("user_route", UserRoutePredicate)
984 config.add_route_predicate("user_route", UserRoutePredicate)
@@ -1,295 +1,295 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-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 RhodeCode authentication plugin for Atlassian CROWD
20 RhodeCode authentication plugin for Atlassian CROWD
21 """
21 """
22
22
23
23
24 import colander
24 import colander
25 import base64
25 import base64
26 import logging
26 import logging
27 import urllib.request
27 import urllib.request
28 import urllib.error
28 import urllib.error
29 import urllib.parse
29 import urllib.parse
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwargs):
43 def plugin_factory(plugin_id, *args, **kwargs):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if 'port' not in kwargs:
113 if 'port' not in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib.request.HTTPBasicAuthHandler(mgr)
133 handler = urllib.request.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib.request.build_opener(handler)
134 self.opener = urllib.request.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("{}:{}".format(self.user, self.passwd))
142 authstring = base64.b64encode("{}:{}".format(self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib.request.Request(url, body, _headers)
149 request = urllib.request.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 ret_doc = self.opener.open(request)
156 ret_doc = self.opener.open(request)
157 msg = ret_doc.read()
157 msg = ret_doc.read()
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 ret_val = {}
159 ret_val = {}
160 ret_val["status"] = True
160 ret_val["status"] = True
161 ret_val["error"] = "Response body was empty"
161 ret_val["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 ret_val = json.loads(msg)
163 ret_val = json.loads(msg)
164 ret_val["status"] = True
164 ret_val["status"] = True
165 else:
165 else:
166 ret_val = msg
166 ret_val = msg
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 ret_val = {"status": False,
169 ret_val = {"status": False,
170 "body": body,
170 "body": body,
171 "error": f"{e}\n{msg}"}
171 "error": f"{e}\n{msg}"}
172 else:
172 else:
173 ret_val = None
173 ret_val = None
174 return ret_val
174 return ret_val
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 uid = 'crowd'
192 uid = 'crowd'
193 _settings_unsafe_keys = ['app_password']
193 _settings_unsafe_keys = ['app_password']
194
194
195 def includeme(self, config):
195 def includeme(self, config):
196 config.add_authn_plugin(self)
196 config.add_authn_plugin(self)
197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
198 config.add_view(
198 config.add_view(
199 'rhodecode.authentication.views.AuthnPluginViewBase',
199 'rhodecode.authentication.views.AuthnPluginViewBase',
200 attr='settings_get',
200 attr='settings_get',
201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
202 request_method='GET',
202 request_method='GET',
203 route_name='auth_home',
203 route_name='auth_home',
204 context=CrowdAuthnResource)
204 context=CrowdAuthnResource)
205 config.add_view(
205 config.add_view(
206 'rhodecode.authentication.views.AuthnPluginViewBase',
206 'rhodecode.authentication.views.AuthnPluginViewBase',
207 attr='settings_post',
207 attr='settings_post',
208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
209 request_method='POST',
209 request_method='POST',
210 route_name='auth_home',
210 route_name='auth_home',
211 context=CrowdAuthnResource)
211 context=CrowdAuthnResource)
212
212
213 def get_settings_schema(self):
213 def get_settings_schema(self):
214 return CrowdSettingsSchema()
214 return CrowdSettingsSchema()
215
215
216 def get_display_name(self, load_from_settings=False):
216 def get_display_name(self, load_from_settings=False):
217 return _('CROWD')
217 return _('CROWD')
218
218
219 @classmethod
219 @classmethod
220 def docs(cls):
220 def docs(cls):
221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
222
222
223 @hybrid_property
223 @hybrid_property
224 def name(self):
224 def name(self):
225 return "crowd"
225 return "crowd"
226
226
227 def use_fake_password(self):
227 def use_fake_password(self):
228 return True
228 return True
229
229
230 def user_activation_state(self):
230 def user_activation_state(self):
231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
232 return 'hg.extern_activate.auto' in def_user_perms
232 return 'hg.extern_activate.auto' in def_user_perms
233
233
234 def auth(self, userobj, username, password, settings, **kwargs):
234 def auth(self, userobj, username, password, settings, **kwargs):
235 """
235 """
236 Given a user object (which may be null), username, a plaintext password,
236 Given a user object (which may be null), username, a plaintext password,
237 and a settings object (containing all the keys needed as listed in settings()),
237 and a settings object (containing all the keys needed as listed in settings()),
238 authenticate this user's login attempt.
238 authenticate this user's login attempt.
239
239
240 Return None on failure. On success, return a dictionary of the form:
240 Return None on failure. On success, return a dictionary of the form:
241
241
242 see: RhodeCodeAuthPluginBase.auth_func_attrs
242 see: RhodeCodeAuthPluginBase.auth_func_attrs
243 This is later validated for correctness
243 This is later validated for correctness
244 """
244 """
245 if not username or not password:
245 if not username or not password:
246 log.debug('Empty username or password skipping...')
246 log.debug('Empty username or password skipping...')
247 return None
247 return None
248
248
249 log.debug("Crowd settings: \n%s", formatted_json(settings))
249 log.debug("Crowd settings: \n%s", formatted_json(settings))
250 server = CrowdServer(**settings)
250 server = CrowdServer(**settings)
251 server.set_credentials(settings["app_name"], settings["app_password"])
251 server.set_credentials(settings["app_name"], settings["app_password"])
252 crowd_user = server.user_auth(username, password)
252 crowd_user = server.user_auth(username, password)
253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
254 if not crowd_user["status"]:
254 if not crowd_user["status"]:
255 return None
255 return None
256
256
257 res = server.user_groups(crowd_user["name"])
257 res = server.user_groups(crowd_user["name"])
258 log.debug("Crowd groups: \n%s", formatted_json(res))
258 log.debug("Crowd groups: \n%s", formatted_json(res))
259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
260
260
261 # old attrs fetched from RhodeCode database
261 # old attrs fetched from RhodeCode database
262 admin = getattr(userobj, 'admin', False)
262 admin = getattr(userobj, 'admin', False)
263 active = getattr(userobj, 'active', True)
263 active = getattr(userobj, 'active', True)
264 email = getattr(userobj, 'email', '')
264 email = getattr(userobj, 'email', '')
265 username = getattr(userobj, 'username', username)
265 username = getattr(userobj, 'username', username)
266 firstname = getattr(userobj, 'firstname', '')
266 firstname = getattr(userobj, 'firstname', '')
267 lastname = getattr(userobj, 'lastname', '')
267 lastname = getattr(userobj, 'lastname', '')
268 extern_type = getattr(userobj, 'extern_type', '')
268 extern_type = getattr(userobj, 'extern_type', '')
269
269
270 user_attrs = {
270 user_attrs = {
271 'username': username,
271 'username': username,
272 'firstname': crowd_user["first-name"] or firstname,
272 'firstname': crowd_user["first-name"] or firstname,
273 'lastname': crowd_user["last-name"] or lastname,
273 'lastname': crowd_user["last-name"] or lastname,
274 'groups': crowd_user["groups"],
274 'groups': crowd_user["groups"],
275 'user_group_sync': True,
275 'user_group_sync': True,
276 'email': crowd_user["email"] or email,
276 'email': crowd_user["email"] or email,
277 'admin': admin,
277 'admin': admin,
278 'active': active,
278 'active': active,
279 'active_from_extern': crowd_user.get('active'),
279 'active_from_extern': crowd_user.get('active'),
280 'extern_name': crowd_user["name"],
280 'extern_name': crowd_user["name"],
281 'extern_type': extern_type,
281 'extern_type': extern_type,
282 }
282 }
283
283
284 # set an admin if we're in admin_groups of crowd
284 # set an admin if we're in admin_groups of crowd
285 for group in settings["admin_groups"]:
285 for group in settings["admin_groups"]:
286 if group in user_attrs["groups"]:
286 if group in user_attrs["groups"]:
287 user_attrs["admin"] = True
287 user_attrs["admin"] = True
288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
289 log.info('user `%s` authenticated correctly', user_attrs['username'])
289 log.info('user `%s` authenticated correctly', user_attrs['username'])
290 return user_attrs
290 return user_attrs
291
291
292
292
293 def includeme(config):
293 def includeme(config):
294 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
294 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
295 plugin_factory(plugin_id).includeme(config)
295 plugin_factory(plugin_id).includeme(config)
@@ -1,173 +1,173 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-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 RhodeCode authentication plugin for Jasig CAS
20 RhodeCode authentication plugin for Jasig CAS
21 http://www.jasig.org/cas
21 http://www.jasig.org/cas
22 """
22 """
23
23
24
24
25 import colander
25 import colander
26 import logging
26 import logging
27 import rhodecode
27 import rhodecode
28 import urllib.request
28 import urllib.request
29 import urllib.parse
29 import urllib.parse
30 import urllib.error
30 import urllib.error
31
31
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.model.db import User
39 from rhodecode.model.db import User
40 from rhodecode.lib.str_utils import safe_str
40 from rhodecode.lib.str_utils import safe_str
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def plugin_factory(plugin_id, *args, **kwargs):
45 def plugin_factory(plugin_id, *args, **kwargs):
46 """
46 """
47 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
48 It returns the plugin instance.
48 It returns the plugin instance.
49 """
49 """
50 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 return plugin
51 return plugin
52
52
53
53
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('URL'),
64 title=_('URL'),
65 widget='string')
65 widget='string')
66
66
67
67
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69 uid = 'jasig_cas'
69 uid = 'jasig_cas'
70
70
71 def includeme(self, config):
71 def includeme(self, config):
72 config.add_authn_plugin(self)
72 config.add_authn_plugin(self)
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
74 config.add_view(
74 config.add_view(
75 'rhodecode.authentication.views.AuthnPluginViewBase',
75 'rhodecode.authentication.views.AuthnPluginViewBase',
76 attr='settings_get',
76 attr='settings_get',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
78 request_method='GET',
78 request_method='GET',
79 route_name='auth_home',
79 route_name='auth_home',
80 context=JasigCasAuthnResource)
80 context=JasigCasAuthnResource)
81 config.add_view(
81 config.add_view(
82 'rhodecode.authentication.views.AuthnPluginViewBase',
82 'rhodecode.authentication.views.AuthnPluginViewBase',
83 attr='settings_post',
83 attr='settings_post',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 request_method='POST',
85 request_method='POST',
86 route_name='auth_home',
86 route_name='auth_home',
87 context=JasigCasAuthnResource)
87 context=JasigCasAuthnResource)
88
88
89 def get_settings_schema(self):
89 def get_settings_schema(self):
90 return JasigCasSettingsSchema()
90 return JasigCasSettingsSchema()
91
91
92 def get_display_name(self, load_from_settings=False):
92 def get_display_name(self, load_from_settings=False):
93 return _('Jasig-CAS')
93 return _('Jasig-CAS')
94
94
95 @hybrid_property
95 @hybrid_property
96 def name(self):
96 def name(self):
97 return "jasig-cas"
97 return "jasig-cas"
98
98
99 @property
99 @property
100 def is_headers_auth(self):
100 def is_headers_auth(self):
101 return True
101 return True
102
102
103 def use_fake_password(self):
103 def use_fake_password(self):
104 return True
104 return True
105
105
106 def user_activation_state(self):
106 def user_activation_state(self):
107 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 def_user_perms = User.get_default_user().AuthUser().permissions['global']
108 return 'hg.extern_activate.auto' in def_user_perms
108 return 'hg.extern_activate.auto' in def_user_perms
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 """
111 """
112 Given a user object (which may be null), username, a plaintext password,
112 Given a user object (which may be null), username, a plaintext password,
113 and a settings object (containing all the keys needed as listed in settings()),
113 and a settings object (containing all the keys needed as listed in settings()),
114 authenticate this user's login attempt.
114 authenticate this user's login attempt.
115
115
116 Return None on failure. On success, return a dictionary of the form:
116 Return None on failure. On success, return a dictionary of the form:
117
117
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 see: RhodeCodeAuthPluginBase.auth_func_attrs
119 This is later validated for correctness
119 This is later validated for correctness
120 """
120 """
121 if not username or not password:
121 if not username or not password:
122 log.debug('Empty username or password skipping...')
122 log.debug('Empty username or password skipping...')
123 return None
123 return None
124
124
125 log.debug("Jasig CAS settings: %s", settings)
125 log.debug("Jasig CAS settings: %s", settings)
126 params = urllib.parse.urlencode({'username': username, 'password': password})
126 params = urllib.parse.urlencode({'username': username, 'password': password})
127 headers = {"Content-type": "application/x-www-form-urlencoded",
127 headers = {"Content-type": "application/x-www-form-urlencoded",
128 "Accept": "text/plain",
128 "Accept": "text/plain",
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
130 url = settings["service_url"]
130 url = settings["service_url"]
131
131
132 log.debug("Sent Jasig CAS: \n%s",
132 log.debug("Sent Jasig CAS: \n%s",
133 {"url": url, "body": params, "headers": headers})
133 {"url": url, "body": params, "headers": headers})
134 request = urllib.request.Request(url, params, headers)
134 request = urllib.request.Request(url, params, headers)
135 try:
135 try:
136 urllib.request.urlopen(request)
136 urllib.request.urlopen(request)
137 except urllib.error.HTTPError as e:
137 except urllib.error.HTTPError as e:
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
138 log.debug("HTTPError when requesting Jasig CAS (status code: %d)", e.code)
139 return None
139 return None
140 except urllib.error.URLError as e:
140 except urllib.error.URLError as e:
141 log.debug("URLError when requesting Jasig CAS url: %s %s", url, e)
141 log.debug("URLError when requesting Jasig CAS url: %s %s", url, e)
142 return None
142 return None
143
143
144 # old attrs fetched from RhodeCode database
144 # old attrs fetched from RhodeCode database
145 admin = getattr(userobj, 'admin', False)
145 admin = getattr(userobj, 'admin', False)
146 active = getattr(userobj, 'active', True)
146 active = getattr(userobj, 'active', True)
147 email = getattr(userobj, 'email', '')
147 email = getattr(userobj, 'email', '')
148 username = getattr(userobj, 'username', username)
148 username = getattr(userobj, 'username', username)
149 firstname = getattr(userobj, 'firstname', '')
149 firstname = getattr(userobj, 'firstname', '')
150 lastname = getattr(userobj, 'lastname', '')
150 lastname = getattr(userobj, 'lastname', '')
151 extern_type = getattr(userobj, 'extern_type', '')
151 extern_type = getattr(userobj, 'extern_type', '')
152
152
153 user_attrs = {
153 user_attrs = {
154 'username': username,
154 'username': username,
155 'firstname': safe_str(firstname or username),
155 'firstname': safe_str(firstname or username),
156 'lastname': safe_str(lastname or ''),
156 'lastname': safe_str(lastname or ''),
157 'groups': [],
157 'groups': [],
158 'user_group_sync': False,
158 'user_group_sync': False,
159 'email': email or '',
159 'email': email or '',
160 'admin': admin or False,
160 'admin': admin or False,
161 'active': active,
161 'active': active,
162 'active_from_extern': True,
162 'active_from_extern': True,
163 'extern_name': username,
163 'extern_name': username,
164 'extern_type': extern_type,
164 'extern_type': extern_type,
165 }
165 }
166
166
167 log.info('user `%s` authenticated correctly', user_attrs['username'])
167 log.info('user `%s` authenticated correctly', user_attrs['username'])
168 return user_attrs
168 return user_attrs
169
169
170
170
171 def includeme(config):
171 def includeme(config):
172 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
172 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
173 plugin_factory(plugin_id).includeme(config)
173 plugin_factory(plugin_id).includeme(config)
@@ -1,550 +1,550 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 RhodeCode authentication plugin for LDAP
20 RhodeCode authentication plugin for LDAP
21 """
21 """
22
22
23 import logging
23 import logging
24 import traceback
24 import traceback
25
25
26 import colander
26 import colander
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.authentication.base import (
28 from rhodecode.authentication.base import (
29 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
29 RhodeCodeExternalAuthPlugin, AuthLdapBase, hybrid_property)
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 from rhodecode.lib.colander_utils import strip_whitespace
32 from rhodecode.lib.colander_utils import strip_whitespace
33 from rhodecode.lib.exceptions import (
33 from rhodecode.lib.exceptions import (
34 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
34 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
35 )
35 )
36 from rhodecode.lib.str_utils import safe_str
36 from rhodecode.lib.str_utils import safe_str
37 from rhodecode.model.db import User
37 from rhodecode.model.db import User
38 from rhodecode.model.validators import Missing
38 from rhodecode.model.validators import Missing
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42 try:
42 try:
43 import ldap
43 import ldap
44 except ImportError:
44 except ImportError:
45 # means that python-ldap is not installed, we use Missing object to mark
45 # means that python-ldap is not installed, we use Missing object to mark
46 # ldap lib is Missing
46 # ldap lib is Missing
47 ldap = Missing
47 ldap = Missing
48
48
49
49
50 class LdapError(Exception):
50 class LdapError(Exception):
51 pass
51 pass
52
52
53
53
54 def plugin_factory(plugin_id, *args, **kwargs):
54 def plugin_factory(plugin_id, *args, **kwargs):
55 """
55 """
56 Factory function that is called during plugin discovery.
56 Factory function that is called during plugin discovery.
57 It returns the plugin instance.
57 It returns the plugin instance.
58 """
58 """
59 plugin = RhodeCodeAuthPlugin(plugin_id)
59 plugin = RhodeCodeAuthPlugin(plugin_id)
60 return plugin
60 return plugin
61
61
62
62
63 class LdapAuthnResource(AuthnPluginResourceBase):
63 class LdapAuthnResource(AuthnPluginResourceBase):
64 pass
64 pass
65
65
66
66
67 class AuthLdap(AuthLdapBase):
67 class AuthLdap(AuthLdapBase):
68 default_tls_cert_dir = '/etc/openldap/cacerts'
68 default_tls_cert_dir = '/etc/openldap/cacerts'
69
69
70 scope_labels = {
70 scope_labels = {
71 ldap.SCOPE_BASE: 'SCOPE_BASE',
71 ldap.SCOPE_BASE: 'SCOPE_BASE',
72 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
72 ldap.SCOPE_ONELEVEL: 'SCOPE_ONELEVEL',
73 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
73 ldap.SCOPE_SUBTREE: 'SCOPE_SUBTREE',
74 }
74 }
75
75
76 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
76 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
77 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
77 tls_kind='PLAIN', tls_reqcert='DEMAND', tls_cert_file=None,
78 tls_cert_dir=None, ldap_version=3,
78 tls_cert_dir=None, ldap_version=3,
79 search_scope='SUBTREE', attr_login='uid',
79 search_scope='SUBTREE', attr_login='uid',
80 ldap_filter='', timeout=None):
80 ldap_filter='', timeout=None):
81 if ldap == Missing:
81 if ldap == Missing:
82 raise LdapImportError("Missing or incompatible ldap library")
82 raise LdapImportError("Missing or incompatible ldap library")
83
83
84 self.debug = False
84 self.debug = False
85 self.timeout = timeout or 60 * 5
85 self.timeout = timeout or 60 * 5
86 self.ldap_version = ldap_version
86 self.ldap_version = ldap_version
87 self.ldap_server_type = 'ldap'
87 self.ldap_server_type = 'ldap'
88
88
89 self.TLS_KIND = tls_kind
89 self.TLS_KIND = tls_kind
90
90
91 if self.TLS_KIND == 'LDAPS':
91 if self.TLS_KIND == 'LDAPS':
92 port = port or 636
92 port = port or 636
93 self.ldap_server_type += 's'
93 self.ldap_server_type += 's'
94
94
95 OPT_X_TLS_DEMAND = 2
95 OPT_X_TLS_DEMAND = 2
96 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
96 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert, OPT_X_TLS_DEMAND)
97 self.TLS_CERT_FILE = tls_cert_file or ''
97 self.TLS_CERT_FILE = tls_cert_file or ''
98 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
98 self.TLS_CERT_DIR = tls_cert_dir or self.default_tls_cert_dir
99
99
100 # split server into list
100 # split server into list
101 self.SERVER_ADDRESSES = self._get_server_list(server)
101 self.SERVER_ADDRESSES = self._get_server_list(server)
102 self.LDAP_SERVER_PORT = port
102 self.LDAP_SERVER_PORT = port
103
103
104 # USE FOR READ ONLY BIND TO LDAP SERVER
104 # USE FOR READ ONLY BIND TO LDAP SERVER
105 self.attr_login = attr_login
105 self.attr_login = attr_login
106
106
107 self.LDAP_BIND_DN = safe_str(bind_dn)
107 self.LDAP_BIND_DN = safe_str(bind_dn)
108 self.LDAP_BIND_PASS = safe_str(bind_pass)
108 self.LDAP_BIND_PASS = safe_str(bind_pass)
109
109
110 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
110 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
111 self.BASE_DN = safe_str(base_dn)
111 self.BASE_DN = safe_str(base_dn)
112 self.LDAP_FILTER = safe_str(ldap_filter)
112 self.LDAP_FILTER = safe_str(ldap_filter)
113
113
114 def _get_ldap_conn(self):
114 def _get_ldap_conn(self):
115
115
116 if self.debug:
116 if self.debug:
117 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
117 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
118
118
119 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
119 if self.TLS_CERT_FILE and hasattr(ldap, 'OPT_X_TLS_CACERTFILE'):
120 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
120 ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, self.TLS_CERT_FILE)
121
121
122 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
122 elif hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
123 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
123 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, self.TLS_CERT_DIR)
124
124
125 if self.TLS_KIND != 'PLAIN':
125 if self.TLS_KIND != 'PLAIN':
126 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
126 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
127
127
128 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
128 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
129 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
129 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
130
130
131 # init connection now
131 # init connection now
132 ldap_servers = self._build_servers(
132 ldap_servers = self._build_servers(
133 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
133 self.ldap_server_type, self.SERVER_ADDRESSES, self.LDAP_SERVER_PORT)
134 log.debug('initializing LDAP connection to:%s', ldap_servers)
134 log.debug('initializing LDAP connection to:%s', ldap_servers)
135 ldap_conn = ldap.initialize(ldap_servers)
135 ldap_conn = ldap.initialize(ldap_servers)
136 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
136 ldap_conn.set_option(ldap.OPT_NETWORK_TIMEOUT, self.timeout)
137 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
137 ldap_conn.set_option(ldap.OPT_TIMEOUT, self.timeout)
138 ldap_conn.timeout = self.timeout
138 ldap_conn.timeout = self.timeout
139
139
140 if self.ldap_version == 2:
140 if self.ldap_version == 2:
141 ldap_conn.protocol = ldap.VERSION2
141 ldap_conn.protocol = ldap.VERSION2
142 else:
142 else:
143 ldap_conn.protocol = ldap.VERSION3
143 ldap_conn.protocol = ldap.VERSION3
144
144
145 if self.TLS_KIND == 'START_TLS':
145 if self.TLS_KIND == 'START_TLS':
146 ldap_conn.start_tls_s()
146 ldap_conn.start_tls_s()
147
147
148 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
148 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
149 log.debug('Trying simple_bind with password and given login DN: %r',
149 log.debug('Trying simple_bind with password and given login DN: %r',
150 self.LDAP_BIND_DN)
150 self.LDAP_BIND_DN)
151 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
151 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
152 log.debug('simple_bind successful')
152 log.debug('simple_bind successful')
153 return ldap_conn
153 return ldap_conn
154
154
155 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
155 def fetch_attrs_from_simple_bind(self, ldap_conn, dn, username, password):
156 scope = ldap.SCOPE_BASE
156 scope = ldap.SCOPE_BASE
157 scope_label = self.scope_labels.get(scope)
157 scope_label = self.scope_labels.get(scope)
158 ldap_filter = '(objectClass=*)'
158 ldap_filter = '(objectClass=*)'
159
159
160 try:
160 try:
161 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
161 log.debug('Trying authenticated search bind with dn: %r SCOPE: %s (and filter: %s)',
162 dn, scope_label, ldap_filter)
162 dn, scope_label, ldap_filter)
163 ldap_conn.simple_bind_s(dn, safe_str(password))
163 ldap_conn.simple_bind_s(dn, safe_str(password))
164 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
164 response = ldap_conn.search_ext_s(dn, scope, ldap_filter, attrlist=['*', '+'])
165
165
166 if not response:
166 if not response:
167 log.error('search bind returned empty results: %r', response)
167 log.error('search bind returned empty results: %r', response)
168 return {}
168 return {}
169 else:
169 else:
170 _dn, attrs = response[0]
170 _dn, attrs = response[0]
171 return attrs
171 return attrs
172
172
173 except ldap.INVALID_CREDENTIALS:
173 except ldap.INVALID_CREDENTIALS:
174 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
174 log.debug("LDAP rejected password for user '%s': %s, org_exc:",
175 username, dn, exc_info=True)
175 username, dn, exc_info=True)
176
176
177 def authenticate_ldap(self, username, password):
177 def authenticate_ldap(self, username, password):
178 """
178 """
179 Authenticate a user via LDAP and return his/her LDAP properties.
179 Authenticate a user via LDAP and return his/her LDAP properties.
180
180
181 Raises AuthenticationError if the credentials are rejected, or
181 Raises AuthenticationError if the credentials are rejected, or
182 EnvironmentError if the LDAP server can't be reached.
182 EnvironmentError if the LDAP server can't be reached.
183
183
184 :param username: username
184 :param username: username
185 :param password: password
185 :param password: password
186 """
186 """
187
187
188 uid = self.get_uid(username, self.SERVER_ADDRESSES)
188 uid = self.get_uid(username, self.SERVER_ADDRESSES)
189 user_attrs = {}
189 user_attrs = {}
190 dn = ''
190 dn = ''
191
191
192 self.validate_password(username, password)
192 self.validate_password(username, password)
193 self.validate_username(username)
193 self.validate_username(username)
194 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
194 scope_label = self.scope_labels.get(self.SEARCH_SCOPE)
195
195
196 ldap_conn = None
196 ldap_conn = None
197 try:
197 try:
198 ldap_conn = self._get_ldap_conn()
198 ldap_conn = self._get_ldap_conn()
199 filter_ = '(&{}({}={}))'.format(
199 filter_ = '(&{}({}={}))'.format(
200 self.LDAP_FILTER, self.attr_login, username)
200 self.LDAP_FILTER, self.attr_login, username)
201 log.debug("Authenticating %r filter %s and scope: %s",
201 log.debug("Authenticating %r filter %s and scope: %s",
202 self.BASE_DN, filter_, scope_label)
202 self.BASE_DN, filter_, scope_label)
203
203
204 ldap_objects = ldap_conn.search_ext_s(
204 ldap_objects = ldap_conn.search_ext_s(
205 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
205 self.BASE_DN, self.SEARCH_SCOPE, filter_, attrlist=['*', '+'])
206
206
207 if not ldap_objects:
207 if not ldap_objects:
208 log.debug("No matching LDAP objects for authentication "
208 log.debug("No matching LDAP objects for authentication "
209 "of UID:'%s' username:(%s)", uid, username)
209 "of UID:'%s' username:(%s)", uid, username)
210 raise ldap.NO_SUCH_OBJECT()
210 raise ldap.NO_SUCH_OBJECT()
211
211
212 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
212 log.debug('Found %s matching ldap object[s], trying to authenticate on each one now...', len(ldap_objects))
213 for (dn, _attrs) in ldap_objects:
213 for (dn, _attrs) in ldap_objects:
214 if dn is None:
214 if dn is None:
215 continue
215 continue
216
216
217 user_attrs = self.fetch_attrs_from_simple_bind(
217 user_attrs = self.fetch_attrs_from_simple_bind(
218 ldap_conn, dn, username, password)
218 ldap_conn, dn, username, password)
219
219
220 if user_attrs:
220 if user_attrs:
221 log.debug('Got authenticated user attributes from DN:%s', dn)
221 log.debug('Got authenticated user attributes from DN:%s', dn)
222 break
222 break
223 else:
223 else:
224 raise LdapPasswordError(
224 raise LdapPasswordError(
225 f'Failed to authenticate user `{username}` with given password')
225 f'Failed to authenticate user `{username}` with given password')
226
226
227 except ldap.NO_SUCH_OBJECT:
227 except ldap.NO_SUCH_OBJECT:
228 log.debug("LDAP says no such user '%s' (%s), org_exc:",
228 log.debug("LDAP says no such user '%s' (%s), org_exc:",
229 uid, username, exc_info=True)
229 uid, username, exc_info=True)
230 raise LdapUsernameError('Unable to find user')
230 raise LdapUsernameError('Unable to find user')
231 except ldap.SERVER_DOWN:
231 except ldap.SERVER_DOWN:
232 org_exc = traceback.format_exc()
232 org_exc = traceback.format_exc()
233 raise LdapConnectionError(
233 raise LdapConnectionError(
234 "LDAP can't access authentication server, org_exc:%s" % org_exc)
234 "LDAP can't access authentication server, org_exc:%s" % org_exc)
235 finally:
235 finally:
236 if ldap_conn:
236 if ldap_conn:
237 log.debug('ldap: connection release')
237 log.debug('ldap: connection release')
238 try:
238 try:
239 ldap_conn.unbind_s()
239 ldap_conn.unbind_s()
240 except Exception:
240 except Exception:
241 # for any reason this can raise exception we must catch it
241 # for any reason this can raise exception we must catch it
242 # to not crush the server
242 # to not crush the server
243 pass
243 pass
244
244
245 return dn, user_attrs
245 return dn, user_attrs
246
246
247
247
248 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
248 class LdapSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
249 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
249 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
250 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
250 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
251 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
251 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
252
252
253 host = colander.SchemaNode(
253 host = colander.SchemaNode(
254 colander.String(),
254 colander.String(),
255 default='',
255 default='',
256 description=_('Host[s] of the LDAP Server \n'
256 description=_('Host[s] of the LDAP Server \n'
257 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
257 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
258 'Multiple servers can be specified using commas'),
258 'Multiple servers can be specified using commas'),
259 preparer=strip_whitespace,
259 preparer=strip_whitespace,
260 title=_('LDAP Host'),
260 title=_('LDAP Host'),
261 widget='string')
261 widget='string')
262 port = colander.SchemaNode(
262 port = colander.SchemaNode(
263 colander.Int(),
263 colander.Int(),
264 default=389,
264 default=389,
265 description=_('Custom port that the LDAP server is listening on. '
265 description=_('Custom port that the LDAP server is listening on. '
266 'Default value is: 389, use 636 for LDAPS (SSL)'),
266 'Default value is: 389, use 636 for LDAPS (SSL)'),
267 preparer=strip_whitespace,
267 preparer=strip_whitespace,
268 title=_('Port'),
268 title=_('Port'),
269 validator=colander.Range(min=0, max=65536),
269 validator=colander.Range(min=0, max=65536),
270 widget='int')
270 widget='int')
271
271
272 timeout = colander.SchemaNode(
272 timeout = colander.SchemaNode(
273 colander.Int(),
273 colander.Int(),
274 default=60 * 5,
274 default=60 * 5,
275 description=_('Timeout for LDAP connection'),
275 description=_('Timeout for LDAP connection'),
276 preparer=strip_whitespace,
276 preparer=strip_whitespace,
277 title=_('Connection timeout'),
277 title=_('Connection timeout'),
278 validator=colander.Range(min=1),
278 validator=colander.Range(min=1),
279 widget='int')
279 widget='int')
280
280
281 dn_user = colander.SchemaNode(
281 dn_user = colander.SchemaNode(
282 colander.String(),
282 colander.String(),
283 default='',
283 default='',
284 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
284 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
285 'e.g., cn=admin,dc=mydomain,dc=com, or '
285 'e.g., cn=admin,dc=mydomain,dc=com, or '
286 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
286 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
287 missing='',
287 missing='',
288 preparer=strip_whitespace,
288 preparer=strip_whitespace,
289 title=_('Bind account'),
289 title=_('Bind account'),
290 widget='string')
290 widget='string')
291 dn_pass = colander.SchemaNode(
291 dn_pass = colander.SchemaNode(
292 colander.String(),
292 colander.String(),
293 default='',
293 default='',
294 description=_('Password to authenticate for given user DN.'),
294 description=_('Password to authenticate for given user DN.'),
295 missing='',
295 missing='',
296 preparer=strip_whitespace,
296 preparer=strip_whitespace,
297 title=_('Bind account password'),
297 title=_('Bind account password'),
298 widget='password')
298 widget='password')
299 tls_kind = colander.SchemaNode(
299 tls_kind = colander.SchemaNode(
300 colander.String(),
300 colander.String(),
301 default=tls_kind_choices[0],
301 default=tls_kind_choices[0],
302 description=_('TLS Type'),
302 description=_('TLS Type'),
303 title=_('Connection Security'),
303 title=_('Connection Security'),
304 validator=colander.OneOf(tls_kind_choices),
304 validator=colander.OneOf(tls_kind_choices),
305 widget='select')
305 widget='select')
306 tls_reqcert = colander.SchemaNode(
306 tls_reqcert = colander.SchemaNode(
307 colander.String(),
307 colander.String(),
308 default=tls_reqcert_choices[0],
308 default=tls_reqcert_choices[0],
309 description=_('Require Cert over TLS?. Self-signed and custom '
309 description=_('Require Cert over TLS?. Self-signed and custom '
310 'certificates can be used when\n `RhodeCode Certificate` '
310 'certificates can be used when\n `RhodeCode Certificate` '
311 'found in admin > settings > system info page is extended.'),
311 'found in admin > settings > system info page is extended.'),
312 title=_('Certificate Checks'),
312 title=_('Certificate Checks'),
313 validator=colander.OneOf(tls_reqcert_choices),
313 validator=colander.OneOf(tls_reqcert_choices),
314 widget='select')
314 widget='select')
315 tls_cert_file = colander.SchemaNode(
315 tls_cert_file = colander.SchemaNode(
316 colander.String(),
316 colander.String(),
317 default='',
317 default='',
318 description=_('This specifies the PEM-format file path containing '
318 description=_('This specifies the PEM-format file path containing '
319 'certificates for use in TLS connection.\n'
319 'certificates for use in TLS connection.\n'
320 'If not specified `TLS Cert dir` will be used'),
320 'If not specified `TLS Cert dir` will be used'),
321 title=_('TLS Cert file'),
321 title=_('TLS Cert file'),
322 missing='',
322 missing='',
323 widget='string')
323 widget='string')
324 tls_cert_dir = colander.SchemaNode(
324 tls_cert_dir = colander.SchemaNode(
325 colander.String(),
325 colander.String(),
326 default=AuthLdap.default_tls_cert_dir,
326 default=AuthLdap.default_tls_cert_dir,
327 description=_('This specifies the path of a directory that contains individual '
327 description=_('This specifies the path of a directory that contains individual '
328 'CA certificates in separate files.'),
328 'CA certificates in separate files.'),
329 title=_('TLS Cert dir'),
329 title=_('TLS Cert dir'),
330 widget='string')
330 widget='string')
331 base_dn = colander.SchemaNode(
331 base_dn = colander.SchemaNode(
332 colander.String(),
332 colander.String(),
333 default='',
333 default='',
334 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
334 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
335 'in it to be replaced with current user username \n'
335 'in it to be replaced with current user username \n'
336 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
336 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
337 missing='',
337 missing='',
338 preparer=strip_whitespace,
338 preparer=strip_whitespace,
339 title=_('Base DN'),
339 title=_('Base DN'),
340 widget='string')
340 widget='string')
341 filter = colander.SchemaNode(
341 filter = colander.SchemaNode(
342 colander.String(),
342 colander.String(),
343 default='',
343 default='',
344 description=_('Filter to narrow results \n'
344 description=_('Filter to narrow results \n'
345 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
345 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
346 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
346 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
347 missing='',
347 missing='',
348 preparer=strip_whitespace,
348 preparer=strip_whitespace,
349 title=_('LDAP Search Filter'),
349 title=_('LDAP Search Filter'),
350 widget='string')
350 widget='string')
351
351
352 search_scope = colander.SchemaNode(
352 search_scope = colander.SchemaNode(
353 colander.String(),
353 colander.String(),
354 default=search_scope_choices[2],
354 default=search_scope_choices[2],
355 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
355 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
356 title=_('LDAP Search Scope'),
356 title=_('LDAP Search Scope'),
357 validator=colander.OneOf(search_scope_choices),
357 validator=colander.OneOf(search_scope_choices),
358 widget='select')
358 widget='select')
359 attr_login = colander.SchemaNode(
359 attr_login = colander.SchemaNode(
360 colander.String(),
360 colander.String(),
361 default='uid',
361 default='uid',
362 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
362 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
363 preparer=strip_whitespace,
363 preparer=strip_whitespace,
364 title=_('Login Attribute'),
364 title=_('Login Attribute'),
365 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
365 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
366 widget='string')
366 widget='string')
367 attr_email = colander.SchemaNode(
367 attr_email = colander.SchemaNode(
368 colander.String(),
368 colander.String(),
369 default='',
369 default='',
370 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
370 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
371 'Emails are a crucial part of RhodeCode. \n'
371 'Emails are a crucial part of RhodeCode. \n'
372 'If possible add a valid email attribute to ldap users.'),
372 'If possible add a valid email attribute to ldap users.'),
373 missing='',
373 missing='',
374 preparer=strip_whitespace,
374 preparer=strip_whitespace,
375 title=_('Email Attribute'),
375 title=_('Email Attribute'),
376 widget='string')
376 widget='string')
377 attr_firstname = colander.SchemaNode(
377 attr_firstname = colander.SchemaNode(
378 colander.String(),
378 colander.String(),
379 default='',
379 default='',
380 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
380 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
381 missing='',
381 missing='',
382 preparer=strip_whitespace,
382 preparer=strip_whitespace,
383 title=_('First Name Attribute'),
383 title=_('First Name Attribute'),
384 widget='string')
384 widget='string')
385 attr_lastname = colander.SchemaNode(
385 attr_lastname = colander.SchemaNode(
386 colander.String(),
386 colander.String(),
387 default='',
387 default='',
388 description=_('LDAP Attribute to map to last name (e.g., sn)'),
388 description=_('LDAP Attribute to map to last name (e.g., sn)'),
389 missing='',
389 missing='',
390 preparer=strip_whitespace,
390 preparer=strip_whitespace,
391 title=_('Last Name Attribute'),
391 title=_('Last Name Attribute'),
392 widget='string')
392 widget='string')
393
393
394
394
395 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
395 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
396 uid = 'ldap'
396 uid = 'ldap'
397 # used to define dynamic binding in the
397 # used to define dynamic binding in the
398 DYNAMIC_BIND_VAR = '$login'
398 DYNAMIC_BIND_VAR = '$login'
399 _settings_unsafe_keys = ['dn_pass']
399 _settings_unsafe_keys = ['dn_pass']
400
400
401 def includeme(self, config):
401 def includeme(self, config):
402 config.add_authn_plugin(self)
402 config.add_authn_plugin(self)
403 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
403 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
404 config.add_view(
404 config.add_view(
405 'rhodecode.authentication.views.AuthnPluginViewBase',
405 'rhodecode.authentication.views.AuthnPluginViewBase',
406 attr='settings_get',
406 attr='settings_get',
407 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
407 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
408 request_method='GET',
408 request_method='GET',
409 route_name='auth_home',
409 route_name='auth_home',
410 context=LdapAuthnResource)
410 context=LdapAuthnResource)
411 config.add_view(
411 config.add_view(
412 'rhodecode.authentication.views.AuthnPluginViewBase',
412 'rhodecode.authentication.views.AuthnPluginViewBase',
413 attr='settings_post',
413 attr='settings_post',
414 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
414 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
415 request_method='POST',
415 request_method='POST',
416 route_name='auth_home',
416 route_name='auth_home',
417 context=LdapAuthnResource)
417 context=LdapAuthnResource)
418
418
419 def get_settings_schema(self):
419 def get_settings_schema(self):
420 return LdapSettingsSchema()
420 return LdapSettingsSchema()
421
421
422 def get_display_name(self, load_from_settings=False):
422 def get_display_name(self, load_from_settings=False):
423 return _('LDAP')
423 return _('LDAP')
424
424
425 @classmethod
425 @classmethod
426 def docs(cls):
426 def docs(cls):
427 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
427 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-ldap.html"
428
428
429 @hybrid_property
429 @hybrid_property
430 def name(self):
430 def name(self):
431 return "ldap"
431 return "ldap"
432
432
433 def use_fake_password(self):
433 def use_fake_password(self):
434 return True
434 return True
435
435
436 def user_activation_state(self):
436 def user_activation_state(self):
437 def_user_perms = User.get_default_user().AuthUser().permissions['global']
437 def_user_perms = User.get_default_user().AuthUser().permissions['global']
438 return 'hg.extern_activate.auto' in def_user_perms
438 return 'hg.extern_activate.auto' in def_user_perms
439
439
440 def try_dynamic_binding(self, username, password, current_args):
440 def try_dynamic_binding(self, username, password, current_args):
441 """
441 """
442 Detects marker inside our original bind, and uses dynamic auth if
442 Detects marker inside our original bind, and uses dynamic auth if
443 present
443 present
444 """
444 """
445
445
446 org_bind = current_args['bind_dn']
446 org_bind = current_args['bind_dn']
447 passwd = current_args['bind_pass']
447 passwd = current_args['bind_pass']
448
448
449 def has_bind_marker(_username):
449 def has_bind_marker(_username):
450 if self.DYNAMIC_BIND_VAR in _username:
450 if self.DYNAMIC_BIND_VAR in _username:
451 return True
451 return True
452
452
453 # we only passed in user with "special" variable
453 # we only passed in user with "special" variable
454 if org_bind and has_bind_marker(org_bind) and not passwd:
454 if org_bind and has_bind_marker(org_bind) and not passwd:
455 log.debug('Using dynamic user/password binding for ldap '
455 log.debug('Using dynamic user/password binding for ldap '
456 'authentication. Replacing `%s` with username',
456 'authentication. Replacing `%s` with username',
457 self.DYNAMIC_BIND_VAR)
457 self.DYNAMIC_BIND_VAR)
458 current_args['bind_dn'] = org_bind.replace(
458 current_args['bind_dn'] = org_bind.replace(
459 self.DYNAMIC_BIND_VAR, username)
459 self.DYNAMIC_BIND_VAR, username)
460 current_args['bind_pass'] = password
460 current_args['bind_pass'] = password
461
461
462 return current_args
462 return current_args
463
463
464 def auth(self, userobj, username, password, settings, **kwargs):
464 def auth(self, userobj, username, password, settings, **kwargs):
465 """
465 """
466 Given a user object (which may be null), username, a plaintext password,
466 Given a user object (which may be null), username, a plaintext password,
467 and a settings object (containing all the keys needed as listed in
467 and a settings object (containing all the keys needed as listed in
468 settings()), authenticate this user's login attempt.
468 settings()), authenticate this user's login attempt.
469
469
470 Return None on failure. On success, return a dictionary of the form:
470 Return None on failure. On success, return a dictionary of the form:
471
471
472 see: RhodeCodeAuthPluginBase.auth_func_attrs
472 see: RhodeCodeAuthPluginBase.auth_func_attrs
473 This is later validated for correctness
473 This is later validated for correctness
474 """
474 """
475
475
476 if not username or not password:
476 if not username or not password:
477 log.debug('Empty username or password skipping...')
477 log.debug('Empty username or password skipping...')
478 return None
478 return None
479
479
480 ldap_args = {
480 ldap_args = {
481 'server': settings.get('host', ''),
481 'server': settings.get('host', ''),
482 'base_dn': settings.get('base_dn', ''),
482 'base_dn': settings.get('base_dn', ''),
483 'port': settings.get('port'),
483 'port': settings.get('port'),
484 'bind_dn': settings.get('dn_user'),
484 'bind_dn': settings.get('dn_user'),
485 'bind_pass': settings.get('dn_pass'),
485 'bind_pass': settings.get('dn_pass'),
486 'tls_kind': settings.get('tls_kind'),
486 'tls_kind': settings.get('tls_kind'),
487 'tls_reqcert': settings.get('tls_reqcert'),
487 'tls_reqcert': settings.get('tls_reqcert'),
488 'tls_cert_file': settings.get('tls_cert_file'),
488 'tls_cert_file': settings.get('tls_cert_file'),
489 'tls_cert_dir': settings.get('tls_cert_dir'),
489 'tls_cert_dir': settings.get('tls_cert_dir'),
490 'search_scope': settings.get('search_scope'),
490 'search_scope': settings.get('search_scope'),
491 'attr_login': settings.get('attr_login'),
491 'attr_login': settings.get('attr_login'),
492 'ldap_version': 3,
492 'ldap_version': 3,
493 'ldap_filter': settings.get('filter'),
493 'ldap_filter': settings.get('filter'),
494 'timeout': settings.get('timeout')
494 'timeout': settings.get('timeout')
495 }
495 }
496
496
497 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
497 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
498
498
499 log.debug('Checking for ldap authentication.')
499 log.debug('Checking for ldap authentication.')
500
500
501 try:
501 try:
502 auth_ldap = AuthLdap(**ldap_args)
502 auth_ldap = AuthLdap(**ldap_args)
503 (user_dn, ldap_attrs) = auth_ldap.authenticate_ldap(username, password)
503 (user_dn, ldap_attrs) = auth_ldap.authenticate_ldap(username, password)
504 log.debug('Got ldap DN response %s', user_dn)
504 log.debug('Got ldap DN response %s', user_dn)
505
505
506 def get_ldap_attr(k) -> str:
506 def get_ldap_attr(k) -> str:
507 return safe_str(ldap_attrs.get(settings.get(k), [b''])[0])
507 return safe_str(ldap_attrs.get(settings.get(k), [b''])[0])
508
508
509 # old attrs fetched from RhodeCode database
509 # old attrs fetched from RhodeCode database
510 admin = getattr(userobj, 'admin', False)
510 admin = getattr(userobj, 'admin', False)
511 active = getattr(userobj, 'active', True)
511 active = getattr(userobj, 'active', True)
512 email = getattr(userobj, 'email', '')
512 email = getattr(userobj, 'email', '')
513 username = getattr(userobj, 'username', username)
513 username = getattr(userobj, 'username', username)
514 firstname = getattr(userobj, 'firstname', '')
514 firstname = getattr(userobj, 'firstname', '')
515 lastname = getattr(userobj, 'lastname', '')
515 lastname = getattr(userobj, 'lastname', '')
516 extern_type = getattr(userobj, 'extern_type', '')
516 extern_type = getattr(userobj, 'extern_type', '')
517
517
518 groups = []
518 groups = []
519
519
520 user_attrs = {
520 user_attrs = {
521 'username': username,
521 'username': username,
522 'firstname': get_ldap_attr('attr_firstname') or firstname,
522 'firstname': get_ldap_attr('attr_firstname') or firstname,
523 'lastname': get_ldap_attr('attr_lastname') or lastname,
523 'lastname': get_ldap_attr('attr_lastname') or lastname,
524 'groups': groups,
524 'groups': groups,
525 'user_group_sync': False,
525 'user_group_sync': False,
526 'email': get_ldap_attr('attr_email') or email,
526 'email': get_ldap_attr('attr_email') or email,
527 'admin': admin,
527 'admin': admin,
528 'active': active,
528 'active': active,
529 'active_from_extern': None,
529 'active_from_extern': None,
530 'extern_name': user_dn,
530 'extern_name': user_dn,
531 'extern_type': extern_type,
531 'extern_type': extern_type,
532 }
532 }
533
533
534 log.debug('ldap user: %s', user_attrs)
534 log.debug('ldap user: %s', user_attrs)
535 log.info('user `%s` authenticated correctly', user_attrs['username'],
535 log.info('user `%s` authenticated correctly', user_attrs['username'],
536 extra={"action": "user_auth_ok", "auth_module": "auth_ldap", "username": user_attrs["username"]})
536 extra={"action": "user_auth_ok", "auth_module": "auth_ldap", "username": user_attrs["username"]})
537
537
538 return user_attrs
538 return user_attrs
539
539
540 except (LdapUsernameError, LdapPasswordError, LdapImportError):
540 except (LdapUsernameError, LdapPasswordError, LdapImportError):
541 log.exception("LDAP related exception")
541 log.exception("LDAP related exception")
542 return None
542 return None
543 except (Exception,):
543 except (Exception,):
544 log.exception("Other exception")
544 log.exception("Other exception")
545 return None
545 return None
546
546
547
547
548 def includeme(config):
548 def includeme(config):
549 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
549 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
550 plugin_factory(plugin_id).includeme(config)
550 plugin_factory(plugin_id).includeme(config)
@@ -1,170 +1,170 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-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 RhodeCode authentication library for PAM
20 RhodeCode authentication library for PAM
21 """
21 """
22
22
23 import colander
23 import colander
24 import grp
24 import grp
25 import logging
25 import logging
26 import pam
26 import pam
27 import pwd
27 import pwd
28 import re
28 import re
29 import socket
29 import socket
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 def plugin_factory(plugin_id, *args, **kwargs):
41 def plugin_factory(plugin_id, *args, **kwargs):
42 """
42 """
43 Factory function that is called during plugin discovery.
43 Factory function that is called during plugin discovery.
44 It returns the plugin instance.
44 It returns the plugin instance.
45 """
45 """
46 plugin = RhodeCodeAuthPlugin(plugin_id)
46 plugin = RhodeCodeAuthPlugin(plugin_id)
47 return plugin
47 return plugin
48
48
49
49
50 class PamAuthnResource(AuthnPluginResourceBase):
50 class PamAuthnResource(AuthnPluginResourceBase):
51 pass
51 pass
52
52
53
53
54 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
54 class PamSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
55 service = colander.SchemaNode(
55 service = colander.SchemaNode(
56 colander.String(),
56 colander.String(),
57 default='login',
57 default='login',
58 description=_('PAM service name to use for authentication.'),
58 description=_('PAM service name to use for authentication.'),
59 preparer=strip_whitespace,
59 preparer=strip_whitespace,
60 title=_('PAM service name'),
60 title=_('PAM service name'),
61 widget='string')
61 widget='string')
62 gecos = colander.SchemaNode(
62 gecos = colander.SchemaNode(
63 colander.String(),
63 colander.String(),
64 default=r'(?P<last_name>.+),\s*(?P<first_name>\w+)',
64 default=r'(?P<last_name>.+),\s*(?P<first_name>\w+)',
65 description=_('Regular expression for extracting user name/email etc. '
65 description=_('Regular expression for extracting user name/email etc. '
66 'from Unix userinfo.'),
66 'from Unix userinfo.'),
67 preparer=strip_whitespace,
67 preparer=strip_whitespace,
68 title=_('Gecos Regex'),
68 title=_('Gecos Regex'),
69 widget='string')
69 widget='string')
70
70
71
71
72 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
72 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
73 uid = 'pam'
73 uid = 'pam'
74 # PAM authentication can be slow. Repository operations involve a lot of
74 # PAM authentication can be slow. Repository operations involve a lot of
75 # auth calls. Little caching helps speedup push/pull operations significantly
75 # auth calls. Little caching helps speedup push/pull operations significantly
76 AUTH_CACHE_TTL = 4
76 AUTH_CACHE_TTL = 4
77
77
78 def includeme(self, config):
78 def includeme(self, config):
79 config.add_authn_plugin(self)
79 config.add_authn_plugin(self)
80 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
80 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
81 config.add_view(
81 config.add_view(
82 'rhodecode.authentication.views.AuthnPluginViewBase',
82 'rhodecode.authentication.views.AuthnPluginViewBase',
83 attr='settings_get',
83 attr='settings_get',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 request_method='GET',
85 request_method='GET',
86 route_name='auth_home',
86 route_name='auth_home',
87 context=PamAuthnResource)
87 context=PamAuthnResource)
88 config.add_view(
88 config.add_view(
89 'rhodecode.authentication.views.AuthnPluginViewBase',
89 'rhodecode.authentication.views.AuthnPluginViewBase',
90 attr='settings_post',
90 attr='settings_post',
91 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
91 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 request_method='POST',
92 request_method='POST',
93 route_name='auth_home',
93 route_name='auth_home',
94 context=PamAuthnResource)
94 context=PamAuthnResource)
95
95
96 def get_display_name(self, load_from_settings=False):
96 def get_display_name(self, load_from_settings=False):
97 return _('PAM')
97 return _('PAM')
98
98
99 @classmethod
99 @classmethod
100 def docs(cls):
100 def docs(cls):
101 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
101 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-pam.html"
102
102
103 @hybrid_property
103 @hybrid_property
104 def name(self):
104 def name(self):
105 return "pam"
105 return "pam"
106
106
107 def get_settings_schema(self):
107 def get_settings_schema(self):
108 return PamSettingsSchema()
108 return PamSettingsSchema()
109
109
110 def use_fake_password(self):
110 def use_fake_password(self):
111 return True
111 return True
112
112
113 def auth(self, userobj, username, password, settings, **kwargs):
113 def auth(self, userobj, username, password, settings, **kwargs):
114 if not username or not password:
114 if not username or not password:
115 log.debug('Empty username or password skipping...')
115 log.debug('Empty username or password skipping...')
116 return None
116 return None
117 _pam = pam.pam()
117 _pam = pam.pam()
118 auth_result = _pam.authenticate(username, password, settings["service"])
118 auth_result = _pam.authenticate(username, password, settings["service"])
119
119
120 if not auth_result:
120 if not auth_result:
121 log.error("PAM was unable to authenticate user: %s", username)
121 log.error("PAM was unable to authenticate user: %s", username)
122 return None
122 return None
123
123
124 log.debug('Got PAM response %s', auth_result)
124 log.debug('Got PAM response %s', auth_result)
125
125
126 # old attrs fetched from RhodeCode database
126 # old attrs fetched from RhodeCode database
127 default_email = "{}@{}".format(username, socket.gethostname())
127 default_email = "{}@{}".format(username, socket.gethostname())
128 admin = getattr(userobj, 'admin', False)
128 admin = getattr(userobj, 'admin', False)
129 active = getattr(userobj, 'active', True)
129 active = getattr(userobj, 'active', True)
130 email = getattr(userobj, 'email', '') or default_email
130 email = getattr(userobj, 'email', '') or default_email
131 username = getattr(userobj, 'username', username)
131 username = getattr(userobj, 'username', username)
132 firstname = getattr(userobj, 'firstname', '')
132 firstname = getattr(userobj, 'firstname', '')
133 lastname = getattr(userobj, 'lastname', '')
133 lastname = getattr(userobj, 'lastname', '')
134 extern_type = getattr(userobj, 'extern_type', '')
134 extern_type = getattr(userobj, 'extern_type', '')
135
135
136 user_attrs = {
136 user_attrs = {
137 'username': username,
137 'username': username,
138 'firstname': firstname,
138 'firstname': firstname,
139 'lastname': lastname,
139 'lastname': lastname,
140 'groups': [g.gr_name for g in grp.getgrall()
140 'groups': [g.gr_name for g in grp.getgrall()
141 if username in g.gr_mem],
141 if username in g.gr_mem],
142 'user_group_sync': True,
142 'user_group_sync': True,
143 'email': email,
143 'email': email,
144 'admin': admin,
144 'admin': admin,
145 'active': active,
145 'active': active,
146 'active_from_extern': None,
146 'active_from_extern': None,
147 'extern_name': username,
147 'extern_name': username,
148 'extern_type': extern_type,
148 'extern_type': extern_type,
149 }
149 }
150
150
151 try:
151 try:
152 user_data = pwd.getpwnam(username)
152 user_data = pwd.getpwnam(username)
153 regex = settings["gecos"]
153 regex = settings["gecos"]
154 match = re.search(regex, user_data.pw_gecos)
154 match = re.search(regex, user_data.pw_gecos)
155 if match:
155 if match:
156 user_attrs["firstname"] = match.group('first_name')
156 user_attrs["firstname"] = match.group('first_name')
157 user_attrs["lastname"] = match.group('last_name')
157 user_attrs["lastname"] = match.group('last_name')
158 except Exception:
158 except Exception:
159 log.warning("Cannot extract additional info for PAM user")
159 log.warning("Cannot extract additional info for PAM user")
160 pass
160 pass
161
161
162 log.debug("pamuser: %s", user_attrs)
162 log.debug("pamuser: %s", user_attrs)
163 log.info('user `%s` authenticated correctly', user_attrs['username'],
163 log.info('user `%s` authenticated correctly', user_attrs['username'],
164 extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]})
164 extra={"action": "user_auth_ok", "auth_module": "auth_pam", "username": user_attrs["username"]})
165 return user_attrs
165 return user_attrs
166
166
167
167
168 def includeme(config):
168 def includeme(config):
169 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
169 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
170 plugin_factory(plugin_id).includeme(config)
170 plugin_factory(plugin_id).includeme(config)
@@ -1,228 +1,219 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-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 RhodeCode authentication plugin for built in internal auth
20 RhodeCode authentication plugin for built in internal auth
21 """
21 """
22
22
23 import logging
23 import logging
24
24
25 import colander
25 import colander
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28 from rhodecode.lib.utils2 import safe_bytes
28 from rhodecode.lib.utils2 import safe_bytes
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
30 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase, TwoFactorAuthnPluginSettingsSchemaMixin
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
32 RhodeCodeAuthPluginBase, hybrid_property, HTTP_TYPE, VCS_TYPE)
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
33 from rhodecode.authentication.routes import AuthnPluginResourceBase
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def plugin_factory(plugin_id, *args, **kwargs):
38 def plugin_factory(plugin_id, *args, **kwargs):
39 plugin = RhodeCodeAuthPlugin(plugin_id)
39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 return plugin
40 return plugin
41
41
42
42
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 pass
44 pass
45
45
46
46
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 uid = 'rhodecode'
48 uid = 'rhodecode'
49 AUTH_RESTRICTION_NONE = 'user_all'
49 AUTH_RESTRICTION_NONE = 'user_all'
50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
50 AUTH_RESTRICTION_SUPER_ADMIN = 'user_super_admin'
51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
51 AUTH_RESTRICTION_SCOPE_ALL = 'scope_all'
52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
52 AUTH_RESTRICTION_SCOPE_HTTP = 'scope_http'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
53 AUTH_RESTRICTION_SCOPE_VCS = 'scope_vcs'
54
54
55 def includeme(self, config):
55 def includeme(self, config):
56 config.add_authn_plugin(self)
56 config.add_authn_plugin(self)
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
57 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_get',
60 attr='settings_get',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='GET',
62 request_method='GET',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65 config.add_view(
65 config.add_view(
66 'rhodecode.authentication.views.AuthnPluginViewBase',
66 'rhodecode.authentication.views.AuthnPluginViewBase',
67 attr='settings_post',
67 attr='settings_post',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
68 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
69 request_method='POST',
69 request_method='POST',
70 route_name='auth_home',
70 route_name='auth_home',
71 context=RhodecodeAuthnResource)
71 context=RhodecodeAuthnResource)
72
72
73 def get_settings_schema(self):
73 def get_settings_schema(self):
74 return RhodeCodeSettingsSchema()
74 return RhodeCodeSettingsSchema()
75
75
76 def get_display_name(self, load_from_settings=False):
76 def get_display_name(self, load_from_settings=False):
77 return _('RhodeCode Internal')
77 return _('RhodeCode Internal')
78
78
79 @classmethod
79 @classmethod
80 def docs(cls):
80 def docs(cls):
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
81 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth.html"
82
82
83 @hybrid_property
83 @hybrid_property
84 def name(self):
84 def name(self):
85 return "rhodecode"
85 return "rhodecode"
86
86
87 def user_activation_state(self):
87 def user_activation_state(self):
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
88 def_user_perms = User.get_default_user().AuthUser().permissions['global']
89 return 'hg.register.auto_activate' in def_user_perms
89 return 'hg.register.auto_activate' in def_user_perms
90
90
91 def allows_authentication_from(
91 def allows_authentication_from(
92 self, user, allows_non_existing_user=True,
92 self, user, allows_non_existing_user=True,
93 allowed_auth_plugins=None, allowed_auth_sources=None):
93 allowed_auth_plugins=None, allowed_auth_sources=None):
94 """
94 """
95 Custom method for this auth that doesn't accept non existing users.
95 Custom method for this auth that doesn't accept non existing users.
96 We know that user exists in our database.
96 We know that user exists in our database.
97 """
97 """
98 allows_non_existing_user = False
98 allows_non_existing_user = False
99 return super().allows_authentication_from(
99 return super().allows_authentication_from(
100 user, allows_non_existing_user=allows_non_existing_user)
100 user, allows_non_existing_user=allows_non_existing_user)
101
101
102 def auth(self, userobj, username, password, settings, **kwargs):
102 def auth(self, userobj, username, password, settings, **kwargs):
103 if not userobj:
103 if not userobj:
104 log.debug('userobj was:%s skipping', userobj)
104 log.debug('userobj was:%s skipping', userobj)
105 return None
105 return None
106
106
107 if userobj.extern_type != self.name:
107 if userobj.extern_type != self.name:
108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
108 log.warning("userobj:%s extern_type mismatch got:`%s` expected:`%s`",
109 userobj, userobj.extern_type, self.name)
109 userobj, userobj.extern_type, self.name)
110 return None
110 return None
111
111
112 # check scope of auth
112 # check scope of auth
113 scope_restriction = settings.get('scope_restriction', '')
113 scope_restriction = settings.get('scope_restriction', '')
114
114
115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
115 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_HTTP \
116 and self.auth_type != HTTP_TYPE:
116 and self.auth_type != HTTP_TYPE:
117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
117 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
118 userobj, self.auth_type, scope_restriction)
118 userobj, self.auth_type, scope_restriction)
119 return None
119 return None
120
120
121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
121 if scope_restriction == self.AUTH_RESTRICTION_SCOPE_VCS \
122 and self.auth_type != VCS_TYPE:
122 and self.auth_type != VCS_TYPE:
123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
123 log.warning("userobj:%s tried scope type %s and scope restriction is set to %s",
124 userobj, self.auth_type, scope_restriction)
124 userobj, self.auth_type, scope_restriction)
125 return None
125 return None
126
126
127 # check super-admin restriction
127 # check super-admin restriction
128 auth_restriction = settings.get('auth_restriction', '')
128 auth_restriction = settings.get('auth_restriction', '')
129
129
130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
130 if auth_restriction == self.AUTH_RESTRICTION_SUPER_ADMIN \
131 and userobj.admin is False:
131 and userobj.admin is False:
132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
132 log.warning("userobj:%s is not super-admin and auth restriction is set to %s",
133 userobj, auth_restriction)
133 userobj, auth_restriction)
134 return None
134 return None
135
135
136 user_attrs = {
136 user_attrs = {
137 "username": userobj.username,
137 "username": userobj.username,
138 "firstname": userobj.firstname,
138 "firstname": userobj.firstname,
139 "lastname": userobj.lastname,
139 "lastname": userobj.lastname,
140 "groups": [],
140 "groups": [],
141 'user_group_sync': False,
141 'user_group_sync': False,
142 "email": userobj.email,
142 "email": userobj.email,
143 "admin": userobj.admin,
143 "admin": userobj.admin,
144 "active": userobj.active,
144 "active": userobj.active,
145 "active_from_extern": userobj.active,
145 "active_from_extern": userobj.active,
146 "extern_name": userobj.user_id,
146 "extern_name": userobj.user_id,
147 "extern_type": userobj.extern_type,
147 "extern_type": userobj.extern_type,
148 }
148 }
149
149
150 log.debug("User attributes:%s", user_attrs)
150 log.debug("User attributes:%s", user_attrs)
151 if userobj.active:
151 if userobj.active:
152 from rhodecode.lib import auth
152 from rhodecode.lib import auth
153 crypto_backend = auth.crypto_backend()
153 crypto_backend = auth.crypto_backend()
154 password_encoded = safe_bytes(password)
154 password_encoded = safe_bytes(password)
155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
155 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
156 password_encoded, userobj.password or '')
156 password_encoded, userobj.password or '')
157
157
158 if password_match and new_hash:
158 if password_match and new_hash:
159 log.debug('user %s properly authenticated, but '
159 log.debug('user %s properly authenticated, but '
160 'requires hash change to bcrypt', userobj)
160 'requires hash change to bcrypt', userobj)
161 # if password match, and we use OLD deprecated hash,
161 # if password match, and we use OLD deprecated hash,
162 # we should migrate this user hash password to the new hash
162 # we should migrate this user hash password to the new hash
163 # we store the new returned by hash_check_with_upgrade function
163 # we store the new returned by hash_check_with_upgrade function
164 user_attrs['_hash_migrate'] = new_hash
164 user_attrs['_hash_migrate'] = new_hash
165
165
166 if userobj.username == User.DEFAULT_USER and userobj.active:
166 if userobj.username == User.DEFAULT_USER and userobj.active:
167 log.info('user `%s` authenticated correctly as anonymous user',
167 log.info('user `%s` authenticated correctly as anonymous user',
168 userobj.username,
168 userobj.username,
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
169 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode_anon", "username": userobj.username})
170 return user_attrs
170 return user_attrs
171
171
172 elif (userobj.username == username or userobj.email == username) and password_match:
172 elif (userobj.username == username or userobj.email == username) and password_match:
173 log.info('user `%s` authenticated correctly', userobj.username,
173 log.info('user `%s` authenticated correctly', userobj.username,
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
174 extra={"action": "user_auth_ok", "auth_module": "auth_rhodecode", "username": userobj.username})
175 return user_attrs
175 return user_attrs
176 log.warning("user `%s` used a wrong password when "
176 log.warning("user `%s` used a wrong password when "
177 "authenticating on this plugin", userobj.username)
177 "authenticating on this plugin", userobj.username)
178 return None
178 return None
179 else:
179 else:
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
180 log.warning('user `%s` failed to authenticate via %s, reason: account not '
181 'active.', username, self.name)
181 'active.', username, self.name)
182 return None
182 return None
183
183
184
184
185 class RhodeCodeSettingsSchema(AuthnPluginSettingsSchemaBase):
185 class RhodeCodeSettingsSchema(TwoFactorAuthnPluginSettingsSchemaMixin, AuthnPluginSettingsSchemaBase):
186 global_2fa = colander.SchemaNode(
187 colander.Bool(),
188 default=False,
189 description=_('Force all users to use two factor authentication by enabling this.'),
190 missing=False,
191 title=_('Global 2FA'),
192 widget='bool',
193 )
194
195 auth_restriction_choices = [
186 auth_restriction_choices = [
196 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
187 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_NONE, 'All users'),
197 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
188 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SUPER_ADMIN, 'Super admins only'),
198 ]
189 ]
199
190
200 auth_scope_choices = [
191 auth_scope_choices = [
201 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
192 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_ALL, 'HTTP and VCS'),
202 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
193 (RhodeCodeAuthPlugin.AUTH_RESTRICTION_SCOPE_HTTP, 'HTTP only'),
203 ]
194 ]
204
195
205 auth_restriction = colander.SchemaNode(
196 auth_restriction = colander.SchemaNode(
206 colander.String(),
197 colander.String(),
207 default=auth_restriction_choices[0],
198 default=auth_restriction_choices[0],
208 description=_('Allowed user types for authentication using this plugin.'),
199 description=_('Allowed user types for authentication using this plugin.'),
209 title=_('User restriction'),
200 title=_('User restriction'),
210 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
201 validator=colander.OneOf([x[0] for x in auth_restriction_choices]),
211 widget='select_with_labels',
202 widget='select_with_labels',
212 choices=auth_restriction_choices
203 choices=auth_restriction_choices
213 )
204 )
214 scope_restriction = colander.SchemaNode(
205 scope_restriction = colander.SchemaNode(
215 colander.String(),
206 colander.String(),
216 default=auth_scope_choices[0],
207 default=auth_scope_choices[0],
217 description=_('Allowed protocols for authentication using this plugin. '
208 description=_('Allowed protocols for authentication using this plugin. '
218 'VCS means GIT/HG/SVN. HTTP is web based login.'),
209 'VCS means GIT/HG/SVN. HTTP is web based login.'),
219 title=_('Scope restriction'),
210 title=_('Scope restriction'),
220 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
211 validator=colander.OneOf([x[0] for x in auth_scope_choices]),
221 widget='select_with_labels',
212 widget='select_with_labels',
222 choices=auth_scope_choices
213 choices=auth_scope_choices
223 )
214 )
224
215
225
216
226 def includeme(config):
217 def includeme(config):
227 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
218 plugin_id = f'egg:rhodecode-enterprise-ce#{RhodeCodeAuthPlugin.uid}'
228 plugin_factory(plugin_id).includeme(config)
219 plugin_factory(plugin_id).includeme(config)
@@ -1,50 +1,64 b''
1 # Copyright (C) 2012-2023 RhodeCode GmbH
1 # Copyright (C) 2012-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 colander
19 import colander
20
20
21 from rhodecode.authentication import plugin_default_auth_ttl
21 from rhodecode.authentication import plugin_default_auth_ttl
22 from rhodecode.translation import _
22 from rhodecode.translation import _
23
23
24
24
25 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
25 class AuthnPluginSettingsSchemaBase(colander.MappingSchema):
26 """
26 """
27 This base schema is intended for use in authentication plugins.
27 This base schema is intended for use in authentication plugins.
28 It adds a few default settings (e.g., "enabled"), so that plugin
28 It adds a few default settings (e.g., "enabled"), so that plugin
29 authors don't have to maintain a bunch of boilerplate.
29 authors don't have to maintain a bunch of boilerplate.
30 """
30 """
31 enabled = colander.SchemaNode(
31 enabled = colander.SchemaNode(
32 colander.Bool(),
32 colander.Bool(),
33 default=False,
33 default=False,
34 description=_('Enable or disable this authentication plugin.'),
34 description=_('Enable or disable this authentication plugin.'),
35 missing=False,
35 missing=False,
36 title=_('Enabled'),
36 title=_('Enabled'),
37 widget='bool',
37 widget='bool',
38 )
38 )
39 cache_ttl = colander.SchemaNode(
39 cache_ttl = colander.SchemaNode(
40 colander.Int(),
40 colander.Int(),
41 default=plugin_default_auth_ttl,
41 default=plugin_default_auth_ttl,
42 description=_('Amount of seconds to cache the authentication and '
42 description=_('Amount of seconds to cache the authentication and '
43 'permissions check response call for this plugin. \n'
43 'permissions check response call for this plugin. \n'
44 'Useful for expensive calls like LDAP to improve the '
44 'Useful for expensive calls like LDAP to improve the '
45 'performance of the system (0 means disabled).'),
45 'performance of the system (0 means disabled).'),
46 missing=0,
46 missing=0,
47 title=_('Auth Cache TTL'),
47 title=_('Auth Cache TTL'),
48 validator=colander.Range(min=0, max=None),
48 validator=colander.Range(min=0, max=None),
49 widget='int',
49 widget='int',
50 )
50 )
51
52
53 class TwoFactorAuthnPluginSettingsSchemaMixin(colander.MappingSchema):
54 """
55 Mixin for extending plugins with two-factor authentication option.
56 """
57 global_2fa = colander.SchemaNode(
58 colander.Bool(),
59 default=False,
60 description=_('Force all users to use two factor authentication with this plugin.'),
61 missing=False,
62 title=_('enforce 2FA for users'),
63 widget='bool',
64 )
@@ -1,6038 +1,6037 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 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY)
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY)
203
203
204 return safe_str(bytes_val)
204 return safe_str(bytes_val)
205
205
206
206
207 class BaseModel(object):
207 class BaseModel(object):
208 """
208 """
209 Base Model for all classes
209 Base Model for all classes
210 """
210 """
211
211
212 @classmethod
212 @classmethod
213 def _get_keys(cls):
213 def _get_keys(cls):
214 """return column names for this model """
214 """return column names for this model """
215 return class_mapper(cls).c.keys()
215 return class_mapper(cls).c.keys()
216
216
217 def get_dict(self):
217 def get_dict(self):
218 """
218 """
219 return dict with keys and values corresponding
219 return dict with keys and values corresponding
220 to this model data """
220 to this model data """
221
221
222 d = {}
222 d = {}
223 for k in self._get_keys():
223 for k in self._get_keys():
224 d[k] = getattr(self, k)
224 d[k] = getattr(self, k)
225
225
226 # also use __json__() if present to get additional fields
226 # also use __json__() if present to get additional fields
227 _json_attr = getattr(self, '__json__', None)
227 _json_attr = getattr(self, '__json__', None)
228 if _json_attr:
228 if _json_attr:
229 # update with attributes from __json__
229 # update with attributes from __json__
230 if callable(_json_attr):
230 if callable(_json_attr):
231 _json_attr = _json_attr()
231 _json_attr = _json_attr()
232 for k, val in _json_attr.items():
232 for k, val in _json_attr.items():
233 d[k] = val
233 d[k] = val
234 return d
234 return d
235
235
236 def get_appstruct(self):
236 def get_appstruct(self):
237 """return list with keys and values tuples corresponding
237 """return list with keys and values tuples corresponding
238 to this model data """
238 to this model data """
239
239
240 lst = []
240 lst = []
241 for k in self._get_keys():
241 for k in self._get_keys():
242 lst.append((k, getattr(self, k),))
242 lst.append((k, getattr(self, k),))
243 return lst
243 return lst
244
244
245 def populate_obj(self, populate_dict):
245 def populate_obj(self, populate_dict):
246 """populate model with data from given populate_dict"""
246 """populate model with data from given populate_dict"""
247
247
248 for k in self._get_keys():
248 for k in self._get_keys():
249 if k in populate_dict:
249 if k in populate_dict:
250 setattr(self, k, populate_dict[k])
250 setattr(self, k, populate_dict[k])
251
251
252 @classmethod
252 @classmethod
253 def query(cls):
253 def query(cls):
254 return Session().query(cls)
254 return Session().query(cls)
255
255
256 @classmethod
256 @classmethod
257 def select(cls, custom_cls=None):
257 def select(cls, custom_cls=None):
258 """
258 """
259 stmt = cls.select().where(cls.user_id==1)
259 stmt = cls.select().where(cls.user_id==1)
260 # optionally
260 # optionally
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 result = cls.execute(stmt) | cls.scalars(stmt)
262 result = cls.execute(stmt) | cls.scalars(stmt)
263 """
263 """
264
264
265 if custom_cls:
265 if custom_cls:
266 stmt = select(custom_cls)
266 stmt = select(custom_cls)
267 else:
267 else:
268 stmt = select(cls)
268 stmt = select(cls)
269 return stmt
269 return stmt
270
270
271 @classmethod
271 @classmethod
272 def execute(cls, stmt):
272 def execute(cls, stmt):
273 return Session().execute(stmt)
273 return Session().execute(stmt)
274
274
275 @classmethod
275 @classmethod
276 def scalars(cls, stmt):
276 def scalars(cls, stmt):
277 return Session().scalars(stmt)
277 return Session().scalars(stmt)
278
278
279 @classmethod
279 @classmethod
280 def get(cls, id_):
280 def get(cls, id_):
281 if id_:
281 if id_:
282 return cls.query().get(id_)
282 return cls.query().get(id_)
283
283
284 @classmethod
284 @classmethod
285 def get_or_404(cls, id_):
285 def get_or_404(cls, id_):
286 from pyramid.httpexceptions import HTTPNotFound
286 from pyramid.httpexceptions import HTTPNotFound
287
287
288 try:
288 try:
289 id_ = int(id_)
289 id_ = int(id_)
290 except (TypeError, ValueError):
290 except (TypeError, ValueError):
291 raise HTTPNotFound()
291 raise HTTPNotFound()
292
292
293 res = cls.query().get(id_)
293 res = cls.query().get(id_)
294 if not res:
294 if not res:
295 raise HTTPNotFound()
295 raise HTTPNotFound()
296 return res
296 return res
297
297
298 @classmethod
298 @classmethod
299 def getAll(cls):
299 def getAll(cls):
300 # deprecated and left for backward compatibility
300 # deprecated and left for backward compatibility
301 return cls.get_all()
301 return cls.get_all()
302
302
303 @classmethod
303 @classmethod
304 def get_all(cls):
304 def get_all(cls):
305 return cls.query().all()
305 return cls.query().all()
306
306
307 @classmethod
307 @classmethod
308 def delete(cls, id_):
308 def delete(cls, id_):
309 obj = cls.query().get(id_)
309 obj = cls.query().get(id_)
310 Session().delete(obj)
310 Session().delete(obj)
311
311
312 @classmethod
312 @classmethod
313 def identity_cache(cls, session, attr_name, value):
313 def identity_cache(cls, session, attr_name, value):
314 exist_in_session = []
314 exist_in_session = []
315 for (item_cls, pkey), instance in session.identity_map.items():
315 for (item_cls, pkey), instance in session.identity_map.items():
316 if cls == item_cls and getattr(instance, attr_name) == value:
316 if cls == item_cls and getattr(instance, attr_name) == value:
317 exist_in_session.append(instance)
317 exist_in_session.append(instance)
318 if exist_in_session:
318 if exist_in_session:
319 if len(exist_in_session) == 1:
319 if len(exist_in_session) == 1:
320 return exist_in_session[0]
320 return exist_in_session[0]
321 log.exception(
321 log.exception(
322 'multiple objects with attr %s and '
322 'multiple objects with attr %s and '
323 'value %s found with same name: %r',
323 'value %s found with same name: %r',
324 attr_name, value, exist_in_session)
324 attr_name, value, exist_in_session)
325
325
326 @property
326 @property
327 def cls_name(self):
327 def cls_name(self):
328 return self.__class__.__name__
328 return self.__class__.__name__
329
329
330 def __repr__(self):
330 def __repr__(self):
331 return f'<DB:{self.cls_name}>'
331 return f'<DB:{self.cls_name}>'
332
332
333
333
334 class RhodeCodeSetting(Base, BaseModel):
334 class RhodeCodeSetting(Base, BaseModel):
335 __tablename__ = 'rhodecode_settings'
335 __tablename__ = 'rhodecode_settings'
336 __table_args__ = (
336 __table_args__ = (
337 UniqueConstraint('app_settings_name'),
337 UniqueConstraint('app_settings_name'),
338 base_table_args
338 base_table_args
339 )
339 )
340
340
341 SETTINGS_TYPES = {
341 SETTINGS_TYPES = {
342 'str': safe_str,
342 'str': safe_str,
343 'int': safe_int,
343 'int': safe_int,
344 'unicode': safe_str,
344 'unicode': safe_str,
345 'bool': str2bool,
345 'bool': str2bool,
346 'list': functools.partial(aslist, sep=',')
346 'list': functools.partial(aslist, sep=',')
347 }
347 }
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 GLOBAL_CONF_KEY = 'app_settings'
349 GLOBAL_CONF_KEY = 'app_settings'
350
350
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355
355
356 def __init__(self, key='', val='', type='unicode'):
356 def __init__(self, key='', val='', type='unicode'):
357 self.app_settings_name = key
357 self.app_settings_name = key
358 self.app_settings_type = type
358 self.app_settings_type = type
359 self.app_settings_value = val
359 self.app_settings_value = val
360
360
361 @validates('_app_settings_value')
361 @validates('_app_settings_value')
362 def validate_settings_value(self, key, val):
362 def validate_settings_value(self, key, val):
363 assert type(val) == str
363 assert type(val) == str
364 return val
364 return val
365
365
366 @hybrid_property
366 @hybrid_property
367 def app_settings_value(self):
367 def app_settings_value(self):
368 v = self._app_settings_value
368 v = self._app_settings_value
369 _type = self.app_settings_type
369 _type = self.app_settings_type
370 if _type:
370 if _type:
371 _type = self.app_settings_type.split('.')[0]
371 _type = self.app_settings_type.split('.')[0]
372 # decode the encrypted value
372 # decode the encrypted value
373 if 'encrypted' in self.app_settings_type:
373 if 'encrypted' in self.app_settings_type:
374 cipher = EncryptedTextValue()
374 cipher = EncryptedTextValue()
375 v = safe_str(cipher.process_result_value(v, None))
375 v = safe_str(cipher.process_result_value(v, None))
376
376
377 converter = self.SETTINGS_TYPES.get(_type) or \
377 converter = self.SETTINGS_TYPES.get(_type) or \
378 self.SETTINGS_TYPES['unicode']
378 self.SETTINGS_TYPES['unicode']
379 return converter(v)
379 return converter(v)
380
380
381 @app_settings_value.setter
381 @app_settings_value.setter
382 def app_settings_value(self, val):
382 def app_settings_value(self, val):
383 """
383 """
384 Setter that will always make sure we use unicode in app_settings_value
384 Setter that will always make sure we use unicode in app_settings_value
385
385
386 :param val:
386 :param val:
387 """
387 """
388 val = safe_str(val)
388 val = safe_str(val)
389 # encode the encrypted value
389 # encode the encrypted value
390 if 'encrypted' in self.app_settings_type:
390 if 'encrypted' in self.app_settings_type:
391 cipher = EncryptedTextValue()
391 cipher = EncryptedTextValue()
392 val = safe_str(cipher.process_bind_param(val, None))
392 val = safe_str(cipher.process_bind_param(val, None))
393 self._app_settings_value = val
393 self._app_settings_value = val
394
394
395 @hybrid_property
395 @hybrid_property
396 def app_settings_type(self):
396 def app_settings_type(self):
397 return self._app_settings_type
397 return self._app_settings_type
398
398
399 @app_settings_type.setter
399 @app_settings_type.setter
400 def app_settings_type(self, val):
400 def app_settings_type(self, val):
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 raise Exception('type must be one of %s got %s'
402 raise Exception('type must be one of %s got %s'
403 % (self.SETTINGS_TYPES.keys(), val))
403 % (self.SETTINGS_TYPES.keys(), val))
404 self._app_settings_type = val
404 self._app_settings_type = val
405
405
406 @classmethod
406 @classmethod
407 def get_by_prefix(cls, prefix):
407 def get_by_prefix(cls, prefix):
408 return RhodeCodeSetting.query()\
408 return RhodeCodeSetting.query()\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 .all()
410 .all()
411
411
412 def __repr__(self):
412 def __repr__(self):
413 return "<%s('%s:%s[%s]')>" % (
413 return "<%s('%s:%s[%s]')>" % (
414 self.cls_name,
414 self.cls_name,
415 self.app_settings_name, self.app_settings_value,
415 self.app_settings_name, self.app_settings_value,
416 self.app_settings_type
416 self.app_settings_type
417 )
417 )
418
418
419
419
420 class RhodeCodeUi(Base, BaseModel):
420 class RhodeCodeUi(Base, BaseModel):
421 __tablename__ = 'rhodecode_ui'
421 __tablename__ = 'rhodecode_ui'
422 __table_args__ = (
422 __table_args__ = (
423 UniqueConstraint('ui_key'),
423 UniqueConstraint('ui_key'),
424 base_table_args
424 base_table_args
425 )
425 )
426 # Sync those values with vcsserver.config.hooks
426 # Sync those values with vcsserver.config.hooks
427
427
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 # HG
429 # HG
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 HOOK_PULL = 'outgoing.pull_logger'
431 HOOK_PULL = 'outgoing.pull_logger'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 HOOK_PUSH = 'changegroup.push_logger'
434 HOOK_PUSH = 'changegroup.push_logger'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
436
436
437 HOOKS_BUILTIN = [
437 HOOKS_BUILTIN = [
438 HOOK_PRE_PULL,
438 HOOK_PRE_PULL,
439 HOOK_PULL,
439 HOOK_PULL,
440 HOOK_PRE_PUSH,
440 HOOK_PRE_PUSH,
441 HOOK_PRETX_PUSH,
441 HOOK_PRETX_PUSH,
442 HOOK_PUSH,
442 HOOK_PUSH,
443 HOOK_PUSH_KEY,
443 HOOK_PUSH_KEY,
444 ]
444 ]
445
445
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 # git part is currently hardcoded.
447 # git part is currently hardcoded.
448
448
449 # SVN PATTERNS
449 # SVN PATTERNS
450 SVN_BRANCH_ID = 'vcs_svn_branch'
450 SVN_BRANCH_ID = 'vcs_svn_branch'
451 SVN_TAG_ID = 'vcs_svn_tag'
451 SVN_TAG_ID = 'vcs_svn_tag'
452
452
453 ui_id = Column(
453 ui_id = Column(
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 primary_key=True)
455 primary_key=True)
456 ui_section = Column(
456 ui_section = Column(
457 "ui_section", String(255), nullable=True, unique=None, default=None)
457 "ui_section", String(255), nullable=True, unique=None, default=None)
458 ui_key = Column(
458 ui_key = Column(
459 "ui_key", String(255), nullable=True, unique=None, default=None)
459 "ui_key", String(255), nullable=True, unique=None, default=None)
460 ui_value = Column(
460 ui_value = Column(
461 "ui_value", String(255), nullable=True, unique=None, default=None)
461 "ui_value", String(255), nullable=True, unique=None, default=None)
462 ui_active = Column(
462 ui_active = Column(
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464
464
465 def __repr__(self):
465 def __repr__(self):
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 self.ui_key, self.ui_value)
467 self.ui_key, self.ui_value)
468
468
469
469
470 class RepoRhodeCodeSetting(Base, BaseModel):
470 class RepoRhodeCodeSetting(Base, BaseModel):
471 __tablename__ = 'repo_rhodecode_settings'
471 __tablename__ = 'repo_rhodecode_settings'
472 __table_args__ = (
472 __table_args__ = (
473 UniqueConstraint(
473 UniqueConstraint(
474 'app_settings_name', 'repository_id',
474 'app_settings_name', 'repository_id',
475 name='uq_repo_rhodecode_setting_name_repo_id'),
475 name='uq_repo_rhodecode_setting_name_repo_id'),
476 base_table_args
476 base_table_args
477 )
477 )
478
478
479 repository_id = Column(
479 repository_id = Column(
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 nullable=False)
481 nullable=False)
482 app_settings_id = Column(
482 app_settings_id = Column(
483 "app_settings_id", Integer(), nullable=False, unique=True,
483 "app_settings_id", Integer(), nullable=False, unique=True,
484 default=None, primary_key=True)
484 default=None, primary_key=True)
485 app_settings_name = Column(
485 app_settings_name = Column(
486 "app_settings_name", String(255), nullable=True, unique=None,
486 "app_settings_name", String(255), nullable=True, unique=None,
487 default=None)
487 default=None)
488 _app_settings_value = Column(
488 _app_settings_value = Column(
489 "app_settings_value", String(4096), nullable=True, unique=None,
489 "app_settings_value", String(4096), nullable=True, unique=None,
490 default=None)
490 default=None)
491 _app_settings_type = Column(
491 _app_settings_type = Column(
492 "app_settings_type", String(255), nullable=True, unique=None,
492 "app_settings_type", String(255), nullable=True, unique=None,
493 default=None)
493 default=None)
494
494
495 repository = relationship('Repository', viewonly=True)
495 repository = relationship('Repository', viewonly=True)
496
496
497 def __init__(self, repository_id, key='', val='', type='unicode'):
497 def __init__(self, repository_id, key='', val='', type='unicode'):
498 self.repository_id = repository_id
498 self.repository_id = repository_id
499 self.app_settings_name = key
499 self.app_settings_name = key
500 self.app_settings_type = type
500 self.app_settings_type = type
501 self.app_settings_value = val
501 self.app_settings_value = val
502
502
503 @validates('_app_settings_value')
503 @validates('_app_settings_value')
504 def validate_settings_value(self, key, val):
504 def validate_settings_value(self, key, val):
505 assert type(val) == str
505 assert type(val) == str
506 return val
506 return val
507
507
508 @hybrid_property
508 @hybrid_property
509 def app_settings_value(self):
509 def app_settings_value(self):
510 v = self._app_settings_value
510 v = self._app_settings_value
511 type_ = self.app_settings_type
511 type_ = self.app_settings_type
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 return converter(v)
514 return converter(v)
515
515
516 @app_settings_value.setter
516 @app_settings_value.setter
517 def app_settings_value(self, val):
517 def app_settings_value(self, val):
518 """
518 """
519 Setter that will always make sure we use unicode in app_settings_value
519 Setter that will always make sure we use unicode in app_settings_value
520
520
521 :param val:
521 :param val:
522 """
522 """
523 self._app_settings_value = safe_str(val)
523 self._app_settings_value = safe_str(val)
524
524
525 @hybrid_property
525 @hybrid_property
526 def app_settings_type(self):
526 def app_settings_type(self):
527 return self._app_settings_type
527 return self._app_settings_type
528
528
529 @app_settings_type.setter
529 @app_settings_type.setter
530 def app_settings_type(self, val):
530 def app_settings_type(self, val):
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 if val not in SETTINGS_TYPES:
532 if val not in SETTINGS_TYPES:
533 raise Exception('type must be one of %s got %s'
533 raise Exception('type must be one of %s got %s'
534 % (SETTINGS_TYPES.keys(), val))
534 % (SETTINGS_TYPES.keys(), val))
535 self._app_settings_type = val
535 self._app_settings_type = val
536
536
537 def __repr__(self):
537 def __repr__(self):
538 return "<%s('%s:%s:%s[%s]')>" % (
538 return "<%s('%s:%s:%s[%s]')>" % (
539 self.cls_name, self.repository.repo_name,
539 self.cls_name, self.repository.repo_name,
540 self.app_settings_name, self.app_settings_value,
540 self.app_settings_name, self.app_settings_value,
541 self.app_settings_type
541 self.app_settings_type
542 )
542 )
543
543
544
544
545 class RepoRhodeCodeUi(Base, BaseModel):
545 class RepoRhodeCodeUi(Base, BaseModel):
546 __tablename__ = 'repo_rhodecode_ui'
546 __tablename__ = 'repo_rhodecode_ui'
547 __table_args__ = (
547 __table_args__ = (
548 UniqueConstraint(
548 UniqueConstraint(
549 'repository_id', 'ui_section', 'ui_key',
549 'repository_id', 'ui_section', 'ui_key',
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 base_table_args
551 base_table_args
552 )
552 )
553
553
554 repository_id = Column(
554 repository_id = Column(
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 nullable=False)
556 nullable=False)
557 ui_id = Column(
557 ui_id = Column(
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 primary_key=True)
559 primary_key=True)
560 ui_section = Column(
560 ui_section = Column(
561 "ui_section", String(255), nullable=True, unique=None, default=None)
561 "ui_section", String(255), nullable=True, unique=None, default=None)
562 ui_key = Column(
562 ui_key = Column(
563 "ui_key", String(255), nullable=True, unique=None, default=None)
563 "ui_key", String(255), nullable=True, unique=None, default=None)
564 ui_value = Column(
564 ui_value = Column(
565 "ui_value", String(255), nullable=True, unique=None, default=None)
565 "ui_value", String(255), nullable=True, unique=None, default=None)
566 ui_active = Column(
566 ui_active = Column(
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568
568
569 repository = relationship('Repository', viewonly=True)
569 repository = relationship('Repository', viewonly=True)
570
570
571 def __repr__(self):
571 def __repr__(self):
572 return '<%s[%s:%s]%s=>%s]>' % (
572 return '<%s[%s:%s]%s=>%s]>' % (
573 self.cls_name, self.repository.repo_name,
573 self.cls_name, self.repository.repo_name,
574 self.ui_section, self.ui_key, self.ui_value)
574 self.ui_section, self.ui_key, self.ui_value)
575
575
576
576
577 class User(Base, BaseModel):
577 class User(Base, BaseModel):
578 __tablename__ = 'users'
578 __tablename__ = 'users'
579 __table_args__ = (
579 __table_args__ = (
580 UniqueConstraint('username'), UniqueConstraint('email'),
580 UniqueConstraint('username'), UniqueConstraint('email'),
581 Index('u_username_idx', 'username'),
581 Index('u_username_idx', 'username'),
582 Index('u_email_idx', 'email'),
582 Index('u_email_idx', 'email'),
583 base_table_args
583 base_table_args
584 )
584 )
585
585
586 DEFAULT_USER = 'default'
586 DEFAULT_USER = 'default'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
589 RECOVERY_CODES_COUNT = 10
589 RECOVERY_CODES_COUNT = 10
590
590
591 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
592 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 username = Column("username", String(255), nullable=True, unique=None, default=None)
593 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 password = Column("password", String(255), nullable=True, unique=None, default=None)
594 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
595 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
596 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
597 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
598 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 _email = Column("email", String(255), nullable=True, unique=None, default=None)
599 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
600 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
601 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
602
602
603 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
604 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
605 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
606 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
607 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
608 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
609
609
610 user_log = relationship('UserLog', back_populates='user')
610 user_log = relationship('UserLog', back_populates='user')
611 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
612
612
613 repositories = relationship('Repository', back_populates='user')
613 repositories = relationship('Repository', back_populates='user')
614 repository_groups = relationship('RepoGroup', back_populates='user')
614 repository_groups = relationship('RepoGroup', back_populates='user')
615 user_groups = relationship('UserGroup', back_populates='user')
615 user_groups = relationship('UserGroup', back_populates='user')
616
616
617 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
618 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
619
619
620 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
621 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
623
623
624 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
625
625
626 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 notifications = relationship('UserNotification', cascade='all', back_populates='user')
627 # notifications assigned to this user
627 # notifications assigned to this user
628 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
629 # comments created by this user
629 # comments created by this user
630 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
631 # user profile extra info
631 # user profile extra info
632 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
633 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
634 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
635 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
636
636
637 # gists
637 # gists
638 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 user_gists = relationship('Gist', cascade='all', back_populates='owner')
639 # user pull requests
639 # user pull requests
640 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
641
641
642 # external identities
642 # external identities
643 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
644 # review rules
644 # review rules
645 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
646
646
647 # artifacts owned
647 # artifacts owned
648 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
649
649
650 # no cascade, set NULL
650 # no cascade, set NULL
651 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
652
652
653 def __repr__(self):
653 def __repr__(self):
654 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
655
655
656 @hybrid_property
656 @hybrid_property
657 def email(self):
657 def email(self):
658 return self._email
658 return self._email
659
659
660 @email.setter
660 @email.setter
661 def email(self, val):
661 def email(self, val):
662 self._email = val.lower() if val else None
662 self._email = val.lower() if val else None
663
663
664 @hybrid_property
664 @hybrid_property
665 def first_name(self):
665 def first_name(self):
666 from rhodecode.lib import helpers as h
666 from rhodecode.lib import helpers as h
667 if self.name:
667 if self.name:
668 return h.escape(self.name)
668 return h.escape(self.name)
669 return self.name
669 return self.name
670
670
671 @hybrid_property
671 @hybrid_property
672 def last_name(self):
672 def last_name(self):
673 from rhodecode.lib import helpers as h
673 from rhodecode.lib import helpers as h
674 if self.lastname:
674 if self.lastname:
675 return h.escape(self.lastname)
675 return h.escape(self.lastname)
676 return self.lastname
676 return self.lastname
677
677
678 @hybrid_property
678 @hybrid_property
679 def api_key(self):
679 def api_key(self):
680 """
680 """
681 Fetch if exist an auth-token with role ALL connected to this user
681 Fetch if exist an auth-token with role ALL connected to this user
682 """
682 """
683 user_auth_token = UserApiKeys.query()\
683 user_auth_token = UserApiKeys.query()\
684 .filter(UserApiKeys.user_id == self.user_id)\
684 .filter(UserApiKeys.user_id == self.user_id)\
685 .filter(or_(UserApiKeys.expires == -1,
685 .filter(or_(UserApiKeys.expires == -1,
686 UserApiKeys.expires >= time.time()))\
686 UserApiKeys.expires >= time.time()))\
687 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
688 if user_auth_token:
688 if user_auth_token:
689 user_auth_token = user_auth_token.api_key
689 user_auth_token = user_auth_token.api_key
690
690
691 return user_auth_token
691 return user_auth_token
692
692
693 @api_key.setter
693 @api_key.setter
694 def api_key(self, val):
694 def api_key(self, val):
695 # don't allow to set API key this is deprecated for now
695 # don't allow to set API key this is deprecated for now
696 self._api_key = None
696 self._api_key = None
697
697
698 @property
698 @property
699 def reviewer_pull_requests(self):
699 def reviewer_pull_requests(self):
700 return PullRequestReviewers.query() \
700 return PullRequestReviewers.query() \
701 .options(joinedload(PullRequestReviewers.pull_request)) \
701 .options(joinedload(PullRequestReviewers.pull_request)) \
702 .filter(PullRequestReviewers.user_id == self.user_id) \
702 .filter(PullRequestReviewers.user_id == self.user_id) \
703 .all()
703 .all()
704
704
705 @property
705 @property
706 def firstname(self):
706 def firstname(self):
707 # alias for future
707 # alias for future
708 return self.name
708 return self.name
709
709
710 @property
710 @property
711 def emails(self):
711 def emails(self):
712 other = UserEmailMap.query()\
712 other = UserEmailMap.query()\
713 .filter(UserEmailMap.user == self) \
713 .filter(UserEmailMap.user == self) \
714 .order_by(UserEmailMap.email_id.asc()) \
714 .order_by(UserEmailMap.email_id.asc()) \
715 .all()
715 .all()
716 return [self.email] + [x.email for x in other]
716 return [self.email] + [x.email for x in other]
717
717
718 def emails_cached(self):
718 def emails_cached(self):
719 emails = []
719 emails = []
720 if self.user_id != self.get_default_user_id():
720 if self.user_id != self.get_default_user_id():
721 emails = UserEmailMap.query()\
721 emails = UserEmailMap.query()\
722 .filter(UserEmailMap.user == self) \
722 .filter(UserEmailMap.user == self) \
723 .order_by(UserEmailMap.email_id.asc())
723 .order_by(UserEmailMap.email_id.asc())
724
724
725 emails = emails.options(
725 emails = emails.options(
726 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
727 )
727 )
728
728
729 return [self.email] + [x.email for x in emails]
729 return [self.email] + [x.email for x in emails]
730
730
731 @property
731 @property
732 def auth_tokens(self):
732 def auth_tokens(self):
733 auth_tokens = self.get_auth_tokens()
733 auth_tokens = self.get_auth_tokens()
734 return [x.api_key for x in auth_tokens]
734 return [x.api_key for x in auth_tokens]
735
735
736 def get_auth_tokens(self):
736 def get_auth_tokens(self):
737 return UserApiKeys.query()\
737 return UserApiKeys.query()\
738 .filter(UserApiKeys.user == self)\
738 .filter(UserApiKeys.user == self)\
739 .order_by(UserApiKeys.user_api_key_id.asc())\
739 .order_by(UserApiKeys.user_api_key_id.asc())\
740 .all()
740 .all()
741
741
742 @LazyProperty
742 @LazyProperty
743 def feed_token(self):
743 def feed_token(self):
744 return self.get_feed_token()
744 return self.get_feed_token()
745
745
746 def get_feed_token(self, cache=True):
746 def get_feed_token(self, cache=True):
747 feed_tokens = UserApiKeys.query()\
747 feed_tokens = UserApiKeys.query()\
748 .filter(UserApiKeys.user == self)\
748 .filter(UserApiKeys.user == self)\
749 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
750 if cache:
750 if cache:
751 feed_tokens = feed_tokens.options(
751 feed_tokens = feed_tokens.options(
752 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
753
753
754 feed_tokens = feed_tokens.all()
754 feed_tokens = feed_tokens.all()
755 if feed_tokens:
755 if feed_tokens:
756 return feed_tokens[0].api_key
756 return feed_tokens[0].api_key
757 return 'NO_FEED_TOKEN_AVAILABLE'
757 return 'NO_FEED_TOKEN_AVAILABLE'
758
758
759 @LazyProperty
759 @LazyProperty
760 def artifact_token(self):
760 def artifact_token(self):
761 return self.get_artifact_token()
761 return self.get_artifact_token()
762
762
763 def get_artifact_token(self, cache=True):
763 def get_artifact_token(self, cache=True):
764 artifacts_tokens = UserApiKeys.query()\
764 artifacts_tokens = UserApiKeys.query()\
765 .filter(UserApiKeys.user == self) \
765 .filter(UserApiKeys.user == self) \
766 .filter(or_(UserApiKeys.expires == -1,
766 .filter(or_(UserApiKeys.expires == -1,
767 UserApiKeys.expires >= time.time())) \
767 UserApiKeys.expires >= time.time())) \
768 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
769
769
770 if cache:
770 if cache:
771 artifacts_tokens = artifacts_tokens.options(
771 artifacts_tokens = artifacts_tokens.options(
772 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
773
773
774 artifacts_tokens = artifacts_tokens.all()
774 artifacts_tokens = artifacts_tokens.all()
775 if artifacts_tokens:
775 if artifacts_tokens:
776 return artifacts_tokens[0].api_key
776 return artifacts_tokens[0].api_key
777 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
778
778
779 def get_or_create_artifact_token(self):
779 def get_or_create_artifact_token(self):
780 artifacts_tokens = UserApiKeys.query()\
780 artifacts_tokens = UserApiKeys.query()\
781 .filter(UserApiKeys.user == self) \
781 .filter(UserApiKeys.user == self) \
782 .filter(or_(UserApiKeys.expires == -1,
782 .filter(or_(UserApiKeys.expires == -1,
783 UserApiKeys.expires >= time.time())) \
783 UserApiKeys.expires >= time.time())) \
784 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
785
785
786 artifacts_tokens = artifacts_tokens.all()
786 artifacts_tokens = artifacts_tokens.all()
787 if artifacts_tokens:
787 if artifacts_tokens:
788 return artifacts_tokens[0].api_key
788 return artifacts_tokens[0].api_key
789 else:
789 else:
790 from rhodecode.model.auth_token import AuthTokenModel
790 from rhodecode.model.auth_token import AuthTokenModel
791 artifact_token = AuthTokenModel().create(
791 artifact_token = AuthTokenModel().create(
792 self, 'auto-generated-artifact-token',
792 self, 'auto-generated-artifact-token',
793 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
794 Session.commit()
794 Session.commit()
795 return artifact_token.api_key
795 return artifact_token.api_key
796
796
797 def is_totp_valid(self, received_code, secret):
797 def is_totp_valid(self, received_code, secret):
798 totp = pyotp.TOTP(secret)
798 totp = pyotp.TOTP(secret)
799 return totp.verify(received_code)
799 return totp.verify(received_code)
800
800
801 def is_2fa_recovery_code_valid(self, received_code, secret):
801 def is_2fa_recovery_code_valid(self, received_code, secret):
802 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
802 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
803 recovery_codes = self.get_2fa_recovery_codes()
803 recovery_codes = self.get_2fa_recovery_codes()
804 if received_code in recovery_codes:
804 if received_code in recovery_codes:
805 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
805 encrypted_recovery_codes.pop(recovery_codes.index(received_code))
806 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
806 self.update_userdata(recovery_codes_2fa=encrypted_recovery_codes)
807 return True
807 return True
808 return False
808 return False
809
809
810 @hybrid_property
810 @hybrid_property
811 def has_forced_2fa(self):
811 def has_forced_2fa(self):
812 """
812 """
813 Checks if 2fa was forced for ALL users (including current one)
813 Checks if 2fa was forced for current user
814 """
814 """
815 from rhodecode.model.settings import SettingsModel
815 from rhodecode.model.settings import SettingsModel
816 # So now we're supporting only auth_rhodecode_global_2fa
816 if value := SettingsModel().get_setting_by_name(f'{self.extern_type}_global_2fa'):
817 if value := SettingsModel().get_setting_by_name('auth_rhodecode_global_2fa'):
818 return value.app_settings_value
817 return value.app_settings_value
819 return False
818 return False
820
819
821 @hybrid_property
820 @hybrid_property
822 def has_enabled_2fa(self):
821 def has_enabled_2fa(self):
823 """
822 """
824 Checks if user enabled 2fa
823 Checks if user enabled 2fa
825 """
824 """
826 if value := self.has_forced_2fa:
825 if value := self.has_forced_2fa:
827 return value
826 return value
828 return self.user_data.get('enabled_2fa', False)
827 return self.user_data.get('enabled_2fa', False)
829
828
830 @has_enabled_2fa.setter
829 @has_enabled_2fa.setter
831 def has_enabled_2fa(self, val):
830 def has_enabled_2fa(self, val):
832 val = str2bool(val)
831 val = str2bool(val)
833 self.update_userdata(enabled_2fa=val)
832 self.update_userdata(enabled_2fa=val)
834 if not val:
833 if not val:
835 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
834 # NOTE: setting to false we clear the user_data to not store any 2fa artifacts
836 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
835 self.update_userdata(secret_2fa=None, recovery_codes_2fa=[], check_2fa=False)
837 Session().commit()
836 Session().commit()
838
837
839 @hybrid_property
838 @hybrid_property
840 def check_2fa_required(self):
839 def check_2fa_required(self):
841 """
840 """
842 Check if check 2fa flag is set for this user
841 Check if check 2fa flag is set for this user
843 """
842 """
844 value = self.user_data.get('check_2fa', False)
843 value = self.user_data.get('check_2fa', False)
845 return value
844 return value
846
845
847 @check_2fa_required.setter
846 @check_2fa_required.setter
848 def check_2fa_required(self, val):
847 def check_2fa_required(self, val):
849 val = str2bool(val)
848 val = str2bool(val)
850 self.update_userdata(check_2fa=val)
849 self.update_userdata(check_2fa=val)
851 Session().commit()
850 Session().commit()
852
851
853 @hybrid_property
852 @hybrid_property
854 def has_seen_2fa_codes(self):
853 def has_seen_2fa_codes(self):
855 """
854 """
856 get the flag about if user has seen 2fa recovery codes
855 get the flag about if user has seen 2fa recovery codes
857 """
856 """
858 value = self.user_data.get('recovery_codes_2fa_seen', False)
857 value = self.user_data.get('recovery_codes_2fa_seen', False)
859 return value
858 return value
860
859
861 @has_seen_2fa_codes.setter
860 @has_seen_2fa_codes.setter
862 def has_seen_2fa_codes(self, val):
861 def has_seen_2fa_codes(self, val):
863 val = str2bool(val)
862 val = str2bool(val)
864 self.update_userdata(recovery_codes_2fa_seen=val)
863 self.update_userdata(recovery_codes_2fa_seen=val)
865 Session().commit()
864 Session().commit()
866
865
867 @hybrid_property
866 @hybrid_property
868 def needs_2fa_configure(self):
867 def needs_2fa_configure(self):
869 """
868 """
870 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
869 Determines if setup2fa has completed for this user. Means he has all needed data for 2fa to work.
871
870
872 Currently this is 2fa enabled and secret exists
871 Currently this is 2fa enabled and secret exists
873 """
872 """
874 if self.has_enabled_2fa:
873 if self.has_enabled_2fa:
875 return not self.user_data.get('secret_2fa')
874 return not self.user_data.get('secret_2fa')
876 return False
875 return False
877
876
878 def init_2fa_recovery_codes(self, persist=True, force=False):
877 def init_2fa_recovery_codes(self, persist=True, force=False):
879 """
878 """
880 Creates 2fa recovery codes
879 Creates 2fa recovery codes
881 """
880 """
882 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
881 recovery_codes = self.user_data.get('recovery_codes_2fa', [])
883 encrypted_codes = []
882 encrypted_codes = []
884 if not recovery_codes or force:
883 if not recovery_codes or force:
885 for _ in range(self.RECOVERY_CODES_COUNT):
884 for _ in range(self.RECOVERY_CODES_COUNT):
886 recovery_code = pyotp.random_base32()
885 recovery_code = pyotp.random_base32()
887 recovery_codes.append(recovery_code)
886 recovery_codes.append(recovery_code)
888 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
887 encrypted_code = enc_utils.encrypt_value(safe_bytes(recovery_code), enc_key=ENCRYPTION_KEY)
889 encrypted_codes.append(safe_str(encrypted_code))
888 encrypted_codes.append(safe_str(encrypted_code))
890 if persist:
889 if persist:
891 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
890 self.update_userdata(recovery_codes_2fa=encrypted_codes, recovery_codes_2fa_seen=False)
892 return recovery_codes
891 return recovery_codes
893 # User should not check the same recovery codes more than once
892 # User should not check the same recovery codes more than once
894 return []
893 return []
895
894
896 def get_2fa_recovery_codes(self):
895 def get_2fa_recovery_codes(self):
897 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
896 encrypted_recovery_codes = self.user_data.get('recovery_codes_2fa', [])
898
897
899 recovery_codes = list(map(
898 recovery_codes = list(map(
900 lambda val: safe_str(
899 lambda val: safe_str(
901 enc_utils.decrypt_value(
900 enc_utils.decrypt_value(
902 val,
901 val,
903 enc_key=ENCRYPTION_KEY
902 enc_key=ENCRYPTION_KEY
904 )),
903 )),
905 encrypted_recovery_codes))
904 encrypted_recovery_codes))
906 return recovery_codes
905 return recovery_codes
907
906
908 def init_secret_2fa(self, persist=True, force=False):
907 def init_secret_2fa(self, persist=True, force=False):
909 secret_2fa = self.user_data.get('secret_2fa')
908 secret_2fa = self.user_data.get('secret_2fa')
910 if not secret_2fa or force:
909 if not secret_2fa or force:
911 secret = pyotp.random_base32()
910 secret = pyotp.random_base32()
912 if persist:
911 if persist:
913 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
912 self.update_userdata(secret_2fa=safe_str(enc_utils.encrypt_value(safe_bytes(secret), enc_key=ENCRYPTION_KEY)))
914 return secret
913 return secret
915 return ''
914 return ''
916
915
917 @hybrid_property
916 @hybrid_property
918 def secret_2fa(self) -> str:
917 def secret_2fa(self) -> str:
919 """
918 """
920 get stored secret for 2fa
919 get stored secret for 2fa
921 """
920 """
922 secret_2fa = self.user_data.get('secret_2fa')
921 secret_2fa = self.user_data.get('secret_2fa')
923 if secret_2fa:
922 if secret_2fa:
924 return safe_str(
923 return safe_str(
925 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY))
924 enc_utils.decrypt_value(secret_2fa, enc_key=ENCRYPTION_KEY))
926 return ''
925 return ''
927
926
928 @secret_2fa.setter
927 @secret_2fa.setter
929 def secret_2fa(self, value: str) -> None:
928 def secret_2fa(self, value: str) -> None:
930 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
929 encrypted_value = enc_utils.encrypt_value(safe_bytes(value), enc_key=ENCRYPTION_KEY)
931 self.update_userdata(secret_2fa=safe_str(encrypted_value))
930 self.update_userdata(secret_2fa=safe_str(encrypted_value))
932
931
933 def regenerate_2fa_recovery_codes(self):
932 def regenerate_2fa_recovery_codes(self):
934 """
933 """
935 Regenerates 2fa recovery codes upon request
934 Regenerates 2fa recovery codes upon request
936 """
935 """
937 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
936 new_recovery_codes = self.init_2fa_recovery_codes(force=True)
938 Session().commit()
937 Session().commit()
939 return new_recovery_codes
938 return new_recovery_codes
940
939
941 @classmethod
940 @classmethod
942 def extra_valid_auth_tokens(cls, user, role=None):
941 def extra_valid_auth_tokens(cls, user, role=None):
943 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
942 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
944 .filter(or_(UserApiKeys.expires == -1,
943 .filter(or_(UserApiKeys.expires == -1,
945 UserApiKeys.expires >= time.time()))
944 UserApiKeys.expires >= time.time()))
946 if role:
945 if role:
947 tokens = tokens.filter(or_(UserApiKeys.role == role,
946 tokens = tokens.filter(or_(UserApiKeys.role == role,
948 UserApiKeys.role == UserApiKeys.ROLE_ALL))
947 UserApiKeys.role == UserApiKeys.ROLE_ALL))
949 return tokens.all()
948 return tokens.all()
950
949
951 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
950 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
952 from rhodecode.lib import auth
951 from rhodecode.lib import auth
953
952
954 log.debug('Trying to authenticate user: %s via auth-token, '
953 log.debug('Trying to authenticate user: %s via auth-token, '
955 'and roles: %s', self, roles)
954 'and roles: %s', self, roles)
956
955
957 if not auth_token:
956 if not auth_token:
958 return False
957 return False
959
958
960 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
959 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
961 tokens_q = UserApiKeys.query()\
960 tokens_q = UserApiKeys.query()\
962 .filter(UserApiKeys.user_id == self.user_id)\
961 .filter(UserApiKeys.user_id == self.user_id)\
963 .filter(or_(UserApiKeys.expires == -1,
962 .filter(or_(UserApiKeys.expires == -1,
964 UserApiKeys.expires >= time.time()))
963 UserApiKeys.expires >= time.time()))
965
964
966 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
965 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
967
966
968 crypto_backend = auth.crypto_backend()
967 crypto_backend = auth.crypto_backend()
969 enc_token_map = {}
968 enc_token_map = {}
970 plain_token_map = {}
969 plain_token_map = {}
971 for token in tokens_q:
970 for token in tokens_q:
972 if token.api_key.startswith(crypto_backend.ENC_PREF):
971 if token.api_key.startswith(crypto_backend.ENC_PREF):
973 enc_token_map[token.api_key] = token
972 enc_token_map[token.api_key] = token
974 else:
973 else:
975 plain_token_map[token.api_key] = token
974 plain_token_map[token.api_key] = token
976 log.debug(
975 log.debug(
977 'Found %s plain and %s encrypted tokens to check for authentication for this user',
976 'Found %s plain and %s encrypted tokens to check for authentication for this user',
978 len(plain_token_map), len(enc_token_map))
977 len(plain_token_map), len(enc_token_map))
979
978
980 # plain token match comes first
979 # plain token match comes first
981 match = plain_token_map.get(auth_token)
980 match = plain_token_map.get(auth_token)
982
981
983 # check encrypted tokens now
982 # check encrypted tokens now
984 if not match:
983 if not match:
985 for token_hash, token in enc_token_map.items():
984 for token_hash, token in enc_token_map.items():
986 # NOTE(marcink): this is expensive to calculate, but most secure
985 # NOTE(marcink): this is expensive to calculate, but most secure
987 if crypto_backend.hash_check(auth_token, token_hash):
986 if crypto_backend.hash_check(auth_token, token_hash):
988 match = token
987 match = token
989 break
988 break
990
989
991 if match:
990 if match:
992 log.debug('Found matching token %s', match)
991 log.debug('Found matching token %s', match)
993 if match.repo_id:
992 if match.repo_id:
994 log.debug('Found scope, checking for scope match of token %s', match)
993 log.debug('Found scope, checking for scope match of token %s', match)
995 if match.repo_id == scope_repo_id:
994 if match.repo_id == scope_repo_id:
996 return True
995 return True
997 else:
996 else:
998 log.debug(
997 log.debug(
999 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
998 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
1000 'and calling scope is:%s, skipping further checks',
999 'and calling scope is:%s, skipping further checks',
1001 match.repo, scope_repo_id)
1000 match.repo, scope_repo_id)
1002 return False
1001 return False
1003 else:
1002 else:
1004 return True
1003 return True
1005
1004
1006 return False
1005 return False
1007
1006
1008 @property
1007 @property
1009 def ip_addresses(self):
1008 def ip_addresses(self):
1010 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1009 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
1011 return [x.ip_addr for x in ret]
1010 return [x.ip_addr for x in ret]
1012
1011
1013 @property
1012 @property
1014 def username_and_name(self):
1013 def username_and_name(self):
1015 return f'{self.username} ({self.first_name} {self.last_name})'
1014 return f'{self.username} ({self.first_name} {self.last_name})'
1016
1015
1017 @property
1016 @property
1018 def username_or_name_or_email(self):
1017 def username_or_name_or_email(self):
1019 full_name = self.full_name if self.full_name != ' ' else None
1018 full_name = self.full_name if self.full_name != ' ' else None
1020 return self.username or full_name or self.email
1019 return self.username or full_name or self.email
1021
1020
1022 @property
1021 @property
1023 def full_name(self):
1022 def full_name(self):
1024 return f'{self.first_name} {self.last_name}'
1023 return f'{self.first_name} {self.last_name}'
1025
1024
1026 @property
1025 @property
1027 def full_name_or_username(self):
1026 def full_name_or_username(self):
1028 return (f'{self.first_name} {self.last_name}'
1027 return (f'{self.first_name} {self.last_name}'
1029 if (self.first_name and self.last_name) else self.username)
1028 if (self.first_name and self.last_name) else self.username)
1030
1029
1031 @property
1030 @property
1032 def full_contact(self):
1031 def full_contact(self):
1033 return f'{self.first_name} {self.last_name} <{self.email}>'
1032 return f'{self.first_name} {self.last_name} <{self.email}>'
1034
1033
1035 @property
1034 @property
1036 def short_contact(self):
1035 def short_contact(self):
1037 return f'{self.first_name} {self.last_name}'
1036 return f'{self.first_name} {self.last_name}'
1038
1037
1039 @property
1038 @property
1040 def is_admin(self):
1039 def is_admin(self):
1041 return self.admin
1040 return self.admin
1042
1041
1043 @property
1042 @property
1044 def language(self):
1043 def language(self):
1045 return self.user_data.get('language')
1044 return self.user_data.get('language')
1046
1045
1047 def AuthUser(self, **kwargs):
1046 def AuthUser(self, **kwargs):
1048 """
1047 """
1049 Returns instance of AuthUser for this user
1048 Returns instance of AuthUser for this user
1050 """
1049 """
1051 from rhodecode.lib.auth import AuthUser
1050 from rhodecode.lib.auth import AuthUser
1052 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1051 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
1053
1052
1054 @hybrid_property
1053 @hybrid_property
1055 def user_data(self):
1054 def user_data(self):
1056 if not self._user_data:
1055 if not self._user_data:
1057 return {}
1056 return {}
1058
1057
1059 try:
1058 try:
1060 return json.loads(self._user_data) or {}
1059 return json.loads(self._user_data) or {}
1061 except TypeError:
1060 except TypeError:
1062 return {}
1061 return {}
1063
1062
1064 @user_data.setter
1063 @user_data.setter
1065 def user_data(self, val):
1064 def user_data(self, val):
1066 if not isinstance(val, dict):
1065 if not isinstance(val, dict):
1067 raise Exception(f'user_data must be dict, got {type(val)}')
1066 raise Exception(f'user_data must be dict, got {type(val)}')
1068 try:
1067 try:
1069 self._user_data = safe_bytes(json.dumps(val))
1068 self._user_data = safe_bytes(json.dumps(val))
1070 except Exception:
1069 except Exception:
1071 log.error(traceback.format_exc())
1070 log.error(traceback.format_exc())
1072
1071
1073 @classmethod
1072 @classmethod
1074 def get(cls, user_id, cache=False):
1073 def get(cls, user_id, cache=False):
1075 if not user_id:
1074 if not user_id:
1076 return
1075 return
1077
1076
1078 user = cls.query()
1077 user = cls.query()
1079 if cache:
1078 if cache:
1080 user = user.options(
1079 user = user.options(
1081 FromCache("sql_cache_short", f"get_users_{user_id}"))
1080 FromCache("sql_cache_short", f"get_users_{user_id}"))
1082 return user.get(user_id)
1081 return user.get(user_id)
1083
1082
1084 @classmethod
1083 @classmethod
1085 def get_by_username(cls, username, case_insensitive=False,
1084 def get_by_username(cls, username, case_insensitive=False,
1086 cache=False):
1085 cache=False):
1087
1086
1088 if case_insensitive:
1087 if case_insensitive:
1089 q = cls.select().where(
1088 q = cls.select().where(
1090 func.lower(cls.username) == func.lower(username))
1089 func.lower(cls.username) == func.lower(username))
1091 else:
1090 else:
1092 q = cls.select().where(cls.username == username)
1091 q = cls.select().where(cls.username == username)
1093
1092
1094 if cache:
1093 if cache:
1095 hash_key = _hash_key(username)
1094 hash_key = _hash_key(username)
1096 q = q.options(
1095 q = q.options(
1097 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1096 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
1098
1097
1099 return cls.execute(q).scalar_one_or_none()
1098 return cls.execute(q).scalar_one_or_none()
1100
1099
1101 @classmethod
1100 @classmethod
1102 def get_by_username_or_primary_email(cls, user_identifier):
1101 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)),
1102 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)))
1103 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()
1104 return cls.execute(cls.select(User).from_statement(qs)).scalar_one_or_none()
1106
1105
1107 @classmethod
1106 @classmethod
1108 def get_by_auth_token(cls, auth_token, cache=False):
1107 def get_by_auth_token(cls, auth_token, cache=False):
1109
1108
1110 q = cls.select(User)\
1109 q = cls.select(User)\
1111 .join(UserApiKeys)\
1110 .join(UserApiKeys)\
1112 .where(UserApiKeys.api_key == auth_token)\
1111 .where(UserApiKeys.api_key == auth_token)\
1113 .where(or_(UserApiKeys.expires == -1,
1112 .where(or_(UserApiKeys.expires == -1,
1114 UserApiKeys.expires >= time.time()))
1113 UserApiKeys.expires >= time.time()))
1115
1114
1116 if cache:
1115 if cache:
1117 q = q.options(
1116 q = q.options(
1118 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1117 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
1119
1118
1120 matched_user = cls.execute(q).scalar_one_or_none()
1119 matched_user = cls.execute(q).scalar_one_or_none()
1121
1120
1122 return matched_user
1121 return matched_user
1123
1122
1124 @classmethod
1123 @classmethod
1125 def get_by_email(cls, email, case_insensitive=False, cache=False):
1124 def get_by_email(cls, email, case_insensitive=False, cache=False):
1126
1125
1127 if case_insensitive:
1126 if case_insensitive:
1128 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1127 q = cls.select().where(func.lower(cls.email) == func.lower(email))
1129 else:
1128 else:
1130 q = cls.select().where(cls.email == email)
1129 q = cls.select().where(cls.email == email)
1131
1130
1132 if cache:
1131 if cache:
1133 email_key = _hash_key(email)
1132 email_key = _hash_key(email)
1134 q = q.options(
1133 q = q.options(
1135 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1134 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
1136
1135
1137 ret = cls.execute(q).scalar_one_or_none()
1136 ret = cls.execute(q).scalar_one_or_none()
1138
1137
1139 if ret is None:
1138 if ret is None:
1140 q = cls.select(UserEmailMap)
1139 q = cls.select(UserEmailMap)
1141 # try fetching in alternate email map
1140 # try fetching in alternate email map
1142 if case_insensitive:
1141 if case_insensitive:
1143 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1142 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
1144 else:
1143 else:
1145 q = q.where(UserEmailMap.email == email)
1144 q = q.where(UserEmailMap.email == email)
1146 q = q.options(joinedload(UserEmailMap.user))
1145 q = q.options(joinedload(UserEmailMap.user))
1147 if cache:
1146 if cache:
1148 q = q.options(
1147 q = q.options(
1149 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1148 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
1150
1149
1151 result = cls.execute(q).scalar_one_or_none()
1150 result = cls.execute(q).scalar_one_or_none()
1152 ret = getattr(result, 'user', None)
1151 ret = getattr(result, 'user', None)
1153
1152
1154 return ret
1153 return ret
1155
1154
1156 @classmethod
1155 @classmethod
1157 def get_from_cs_author(cls, author):
1156 def get_from_cs_author(cls, author):
1158 """
1157 """
1159 Tries to get User objects out of commit author string
1158 Tries to get User objects out of commit author string
1160
1159
1161 :param author:
1160 :param author:
1162 """
1161 """
1163 from rhodecode.lib.helpers import email, author_name
1162 from rhodecode.lib.helpers import email, author_name
1164 # Valid email in the attribute passed, see if they're in the system
1163 # Valid email in the attribute passed, see if they're in the system
1165 _email = email(author)
1164 _email = email(author)
1166 if _email:
1165 if _email:
1167 user = cls.get_by_email(_email, case_insensitive=True)
1166 user = cls.get_by_email(_email, case_insensitive=True)
1168 if user:
1167 if user:
1169 return user
1168 return user
1170 # Maybe we can match by username?
1169 # Maybe we can match by username?
1171 _author = author_name(author)
1170 _author = author_name(author)
1172 user = cls.get_by_username(_author, case_insensitive=True)
1171 user = cls.get_by_username(_author, case_insensitive=True)
1173 if user:
1172 if user:
1174 return user
1173 return user
1175
1174
1176 def update_userdata(self, **kwargs):
1175 def update_userdata(self, **kwargs):
1177 usr = self
1176 usr = self
1178 old = usr.user_data
1177 old = usr.user_data
1179 old.update(**kwargs)
1178 old.update(**kwargs)
1180 usr.user_data = old
1179 usr.user_data = old
1181 Session().add(usr)
1180 Session().add(usr)
1182 log.debug('updated userdata with %s', kwargs)
1181 log.debug('updated userdata with %s', kwargs)
1183
1182
1184 def update_lastlogin(self):
1183 def update_lastlogin(self):
1185 """Update user lastlogin"""
1184 """Update user lastlogin"""
1186 self.last_login = datetime.datetime.now()
1185 self.last_login = datetime.datetime.now()
1187 Session().add(self)
1186 Session().add(self)
1188 log.debug('updated user %s lastlogin', self.username)
1187 log.debug('updated user %s lastlogin', self.username)
1189
1188
1190 def update_password(self, new_password):
1189 def update_password(self, new_password):
1191 from rhodecode.lib.auth import get_crypt_password
1190 from rhodecode.lib.auth import get_crypt_password
1192
1191
1193 self.password = get_crypt_password(new_password)
1192 self.password = get_crypt_password(new_password)
1194 Session().add(self)
1193 Session().add(self)
1195
1194
1196 @classmethod
1195 @classmethod
1197 def get_first_super_admin(cls):
1196 def get_first_super_admin(cls):
1198 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1197 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1199 user = cls.scalars(stmt).first()
1198 user = cls.scalars(stmt).first()
1200
1199
1201 if user is None:
1200 if user is None:
1202 raise Exception('FATAL: Missing administrative account!')
1201 raise Exception('FATAL: Missing administrative account!')
1203 return user
1202 return user
1204
1203
1205 @classmethod
1204 @classmethod
1206 def get_all_super_admins(cls, only_active=False):
1205 def get_all_super_admins(cls, only_active=False):
1207 """
1206 """
1208 Returns all admin accounts sorted by username
1207 Returns all admin accounts sorted by username
1209 """
1208 """
1210 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1209 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1211 if only_active:
1210 if only_active:
1212 qry = qry.filter(User.active == true())
1211 qry = qry.filter(User.active == true())
1213 return qry.all()
1212 return qry.all()
1214
1213
1215 @classmethod
1214 @classmethod
1216 def get_all_user_ids(cls, only_active=True):
1215 def get_all_user_ids(cls, only_active=True):
1217 """
1216 """
1218 Returns all users IDs
1217 Returns all users IDs
1219 """
1218 """
1220 qry = Session().query(User.user_id)
1219 qry = Session().query(User.user_id)
1221
1220
1222 if only_active:
1221 if only_active:
1223 qry = qry.filter(User.active == true())
1222 qry = qry.filter(User.active == true())
1224 return [x.user_id for x in qry]
1223 return [x.user_id for x in qry]
1225
1224
1226 @classmethod
1225 @classmethod
1227 def get_default_user(cls, cache=False, refresh=False):
1226 def get_default_user(cls, cache=False, refresh=False):
1228 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1227 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1229 if user is None:
1228 if user is None:
1230 raise Exception('FATAL: Missing default account!')
1229 raise Exception('FATAL: Missing default account!')
1231 if refresh:
1230 if refresh:
1232 # The default user might be based on outdated state which
1231 # The default user might be based on outdated state which
1233 # has been loaded from the cache.
1232 # has been loaded from the cache.
1234 # A call to refresh() ensures that the
1233 # A call to refresh() ensures that the
1235 # latest state from the database is used.
1234 # latest state from the database is used.
1236 Session().refresh(user)
1235 Session().refresh(user)
1237
1236
1238 return user
1237 return user
1239
1238
1240 @classmethod
1239 @classmethod
1241 def get_default_user_id(cls):
1240 def get_default_user_id(cls):
1242 import rhodecode
1241 import rhodecode
1243 return rhodecode.CONFIG['default_user_id']
1242 return rhodecode.CONFIG['default_user_id']
1244
1243
1245 def _get_default_perms(self, user, suffix=''):
1244 def _get_default_perms(self, user, suffix=''):
1246 from rhodecode.model.permission import PermissionModel
1245 from rhodecode.model.permission import PermissionModel
1247 return PermissionModel().get_default_perms(user.user_perms, suffix)
1246 return PermissionModel().get_default_perms(user.user_perms, suffix)
1248
1247
1249 def get_default_perms(self, suffix=''):
1248 def get_default_perms(self, suffix=''):
1250 return self._get_default_perms(self, suffix)
1249 return self._get_default_perms(self, suffix)
1251
1250
1252 def get_api_data(self, include_secrets=False, details='full'):
1251 def get_api_data(self, include_secrets=False, details='full'):
1253 """
1252 """
1254 Common function for generating user related data for API
1253 Common function for generating user related data for API
1255
1254
1256 :param include_secrets: By default secrets in the API data will be replaced
1255 :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
1256 by a placeholder value to prevent exposing this data by accident. In case
1258 this data shall be exposed, set this flag to ``True``.
1257 this data shall be exposed, set this flag to ``True``.
1259
1258
1260 :param details: details can be 'basic|full' basic gives only a subset of
1259 :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.
1260 the available user information that includes user_id, name and emails.
1262 """
1261 """
1263 user = self
1262 user = self
1264 user_data = self.user_data
1263 user_data = self.user_data
1265 data = {
1264 data = {
1266 'user_id': user.user_id,
1265 'user_id': user.user_id,
1267 'username': user.username,
1266 'username': user.username,
1268 'firstname': user.name,
1267 'firstname': user.name,
1269 'lastname': user.lastname,
1268 'lastname': user.lastname,
1270 'description': user.description,
1269 'description': user.description,
1271 'email': user.email,
1270 'email': user.email,
1272 'emails': user.emails,
1271 'emails': user.emails,
1273 }
1272 }
1274 if details == 'basic':
1273 if details == 'basic':
1275 return data
1274 return data
1276
1275
1277 auth_token_length = 40
1276 auth_token_length = 40
1278 auth_token_replacement = '*' * auth_token_length
1277 auth_token_replacement = '*' * auth_token_length
1279
1278
1280 extras = {
1279 extras = {
1281 'auth_tokens': [auth_token_replacement],
1280 'auth_tokens': [auth_token_replacement],
1282 'active': user.active,
1281 'active': user.active,
1283 'admin': user.admin,
1282 'admin': user.admin,
1284 'extern_type': user.extern_type,
1283 'extern_type': user.extern_type,
1285 'extern_name': user.extern_name,
1284 'extern_name': user.extern_name,
1286 'last_login': user.last_login,
1285 'last_login': user.last_login,
1287 'last_activity': user.last_activity,
1286 'last_activity': user.last_activity,
1288 'ip_addresses': user.ip_addresses,
1287 'ip_addresses': user.ip_addresses,
1289 'language': user_data.get('language')
1288 'language': user_data.get('language')
1290 }
1289 }
1291 data.update(extras)
1290 data.update(extras)
1292
1291
1293 if include_secrets:
1292 if include_secrets:
1294 data['auth_tokens'] = user.auth_tokens
1293 data['auth_tokens'] = user.auth_tokens
1295 return data
1294 return data
1296
1295
1297 def __json__(self):
1296 def __json__(self):
1298 data = {
1297 data = {
1299 'full_name': self.full_name,
1298 'full_name': self.full_name,
1300 'full_name_or_username': self.full_name_or_username,
1299 'full_name_or_username': self.full_name_or_username,
1301 'short_contact': self.short_contact,
1300 'short_contact': self.short_contact,
1302 'full_contact': self.full_contact,
1301 'full_contact': self.full_contact,
1303 }
1302 }
1304 data.update(self.get_api_data())
1303 data.update(self.get_api_data())
1305 return data
1304 return data
1306
1305
1307
1306
1308 class UserApiKeys(Base, BaseModel):
1307 class UserApiKeys(Base, BaseModel):
1309 __tablename__ = 'user_api_keys'
1308 __tablename__ = 'user_api_keys'
1310 __table_args__ = (
1309 __table_args__ = (
1311 Index('uak_api_key_idx', 'api_key'),
1310 Index('uak_api_key_idx', 'api_key'),
1312 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1311 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1313 base_table_args
1312 base_table_args
1314 )
1313 )
1315
1314
1316 # ApiKey role
1315 # ApiKey role
1317 ROLE_ALL = 'token_role_all'
1316 ROLE_ALL = 'token_role_all'
1318 ROLE_VCS = 'token_role_vcs'
1317 ROLE_VCS = 'token_role_vcs'
1319 ROLE_API = 'token_role_api'
1318 ROLE_API = 'token_role_api'
1320 ROLE_HTTP = 'token_role_http'
1319 ROLE_HTTP = 'token_role_http'
1321 ROLE_FEED = 'token_role_feed'
1320 ROLE_FEED = 'token_role_feed'
1322 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1321 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1323 # The last one is ignored in the list as we only
1322 # The last one is ignored in the list as we only
1324 # use it for one action, and cannot be created by users
1323 # use it for one action, and cannot be created by users
1325 ROLE_PASSWORD_RESET = 'token_password_reset'
1324 ROLE_PASSWORD_RESET = 'token_password_reset'
1326
1325
1327 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1326 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1328
1327
1329 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1328 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)
1329 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)
1330 api_key = Column("api_key", String(255), nullable=False, unique=True)
1332 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1331 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1333 expires = Column('expires', Float(53), nullable=False)
1332 expires = Column('expires', Float(53), nullable=False)
1334 role = Column('role', String(255), nullable=True)
1333 role = Column('role', String(255), nullable=True)
1335 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1334 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1336
1335
1337 # scope columns
1336 # scope columns
1338 repo_id = Column(
1337 repo_id = Column(
1339 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1338 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1340 nullable=True, unique=None, default=None)
1339 nullable=True, unique=None, default=None)
1341 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1340 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1342
1341
1343 repo_group_id = Column(
1342 repo_group_id = Column(
1344 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1343 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1345 nullable=True, unique=None, default=None)
1344 nullable=True, unique=None, default=None)
1346 repo_group = relationship('RepoGroup', lazy='joined')
1345 repo_group = relationship('RepoGroup', lazy='joined')
1347
1346
1348 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1347 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1349
1348
1350 def __repr__(self):
1349 def __repr__(self):
1351 return f"<{self.cls_name}('{self.role}')>"
1350 return f"<{self.cls_name}('{self.role}')>"
1352
1351
1353 def __json__(self):
1352 def __json__(self):
1354 data = {
1353 data = {
1355 'auth_token': self.api_key,
1354 'auth_token': self.api_key,
1356 'role': self.role,
1355 'role': self.role,
1357 'scope': self.scope_humanized,
1356 'scope': self.scope_humanized,
1358 'expired': self.expired
1357 'expired': self.expired
1359 }
1358 }
1360 return data
1359 return data
1361
1360
1362 def get_api_data(self, include_secrets=False):
1361 def get_api_data(self, include_secrets=False):
1363 data = self.__json__()
1362 data = self.__json__()
1364 if include_secrets:
1363 if include_secrets:
1365 return data
1364 return data
1366 else:
1365 else:
1367 data['auth_token'] = self.token_obfuscated
1366 data['auth_token'] = self.token_obfuscated
1368 return data
1367 return data
1369
1368
1370 @hybrid_property
1369 @hybrid_property
1371 def description_safe(self):
1370 def description_safe(self):
1372 from rhodecode.lib import helpers as h
1371 from rhodecode.lib import helpers as h
1373 return h.escape(self.description)
1372 return h.escape(self.description)
1374
1373
1375 @property
1374 @property
1376 def expired(self):
1375 def expired(self):
1377 if self.expires == -1:
1376 if self.expires == -1:
1378 return False
1377 return False
1379 return time.time() > self.expires
1378 return time.time() > self.expires
1380
1379
1381 @classmethod
1380 @classmethod
1382 def _get_role_name(cls, role):
1381 def _get_role_name(cls, role):
1383 return {
1382 return {
1384 cls.ROLE_ALL: _('all'),
1383 cls.ROLE_ALL: _('all'),
1385 cls.ROLE_HTTP: _('http/web interface'),
1384 cls.ROLE_HTTP: _('http/web interface'),
1386 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1385 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1387 cls.ROLE_API: _('api calls'),
1386 cls.ROLE_API: _('api calls'),
1388 cls.ROLE_FEED: _('feed access'),
1387 cls.ROLE_FEED: _('feed access'),
1389 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1388 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1390 }.get(role, role)
1389 }.get(role, role)
1391
1390
1392 @classmethod
1391 @classmethod
1393 def _get_role_description(cls, role):
1392 def _get_role_description(cls, role):
1394 return {
1393 return {
1395 cls.ROLE_ALL: _('Token for all actions.'),
1394 cls.ROLE_ALL: _('Token for all actions.'),
1396 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1395 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1397 'login using `api_access_controllers_whitelist` functionality.'),
1396 'login using `api_access_controllers_whitelist` functionality.'),
1398 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1397 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1399 'Requires auth_token authentication plugin to be active. <br/>'
1398 'Requires auth_token authentication plugin to be active. <br/>'
1400 'Such Token should be used then instead of a password to '
1399 'Such Token should be used then instead of a password to '
1401 'interact with a repository, and additionally can be '
1400 'interact with a repository, and additionally can be '
1402 'limited to single repository using repo scope.'),
1401 'limited to single repository using repo scope.'),
1403 cls.ROLE_API: _('Token limited to api calls.'),
1402 cls.ROLE_API: _('Token limited to api calls.'),
1404 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1403 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1405 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1404 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1406 }.get(role, role)
1405 }.get(role, role)
1407
1406
1408 @property
1407 @property
1409 def role_humanized(self):
1408 def role_humanized(self):
1410 return self._get_role_name(self.role)
1409 return self._get_role_name(self.role)
1411
1410
1412 def _get_scope(self):
1411 def _get_scope(self):
1413 if self.repo:
1412 if self.repo:
1414 return 'Repository: {}'.format(self.repo.repo_name)
1413 return 'Repository: {}'.format(self.repo.repo_name)
1415 if self.repo_group:
1414 if self.repo_group:
1416 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1415 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1417 return 'Global'
1416 return 'Global'
1418
1417
1419 @property
1418 @property
1420 def scope_humanized(self):
1419 def scope_humanized(self):
1421 return self._get_scope()
1420 return self._get_scope()
1422
1421
1423 @property
1422 @property
1424 def token_obfuscated(self):
1423 def token_obfuscated(self):
1425 if self.api_key:
1424 if self.api_key:
1426 return self.api_key[:4] + "****"
1425 return self.api_key[:4] + "****"
1427
1426
1428
1427
1429 class UserEmailMap(Base, BaseModel):
1428 class UserEmailMap(Base, BaseModel):
1430 __tablename__ = 'user_email_map'
1429 __tablename__ = 'user_email_map'
1431 __table_args__ = (
1430 __table_args__ = (
1432 Index('uem_email_idx', 'email'),
1431 Index('uem_email_idx', 'email'),
1433 Index('uem_user_id_idx', 'user_id'),
1432 Index('uem_user_id_idx', 'user_id'),
1434 UniqueConstraint('email'),
1433 UniqueConstraint('email'),
1435 base_table_args
1434 base_table_args
1436 )
1435 )
1437
1436
1438 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1437 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)
1438 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)
1439 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1441 user = relationship('User', lazy='joined', back_populates='user_emails')
1440 user = relationship('User', lazy='joined', back_populates='user_emails')
1442
1441
1443 @validates('_email')
1442 @validates('_email')
1444 def validate_email(self, key, email):
1443 def validate_email(self, key, email):
1445 # check if this email is not main one
1444 # check if this email is not main one
1446 main_email = Session().query(User).filter(User.email == email).scalar()
1445 main_email = Session().query(User).filter(User.email == email).scalar()
1447 if main_email is not None:
1446 if main_email is not None:
1448 raise AttributeError('email %s is present is user table' % email)
1447 raise AttributeError('email %s is present is user table' % email)
1449 return email
1448 return email
1450
1449
1451 @hybrid_property
1450 @hybrid_property
1452 def email(self):
1451 def email(self):
1453 return self._email
1452 return self._email
1454
1453
1455 @email.setter
1454 @email.setter
1456 def email(self, val):
1455 def email(self, val):
1457 self._email = val.lower() if val else None
1456 self._email = val.lower() if val else None
1458
1457
1459
1458
1460 class UserIpMap(Base, BaseModel):
1459 class UserIpMap(Base, BaseModel):
1461 __tablename__ = 'user_ip_map'
1460 __tablename__ = 'user_ip_map'
1462 __table_args__ = (
1461 __table_args__ = (
1463 UniqueConstraint('user_id', 'ip_addr'),
1462 UniqueConstraint('user_id', 'ip_addr'),
1464 base_table_args
1463 base_table_args
1465 )
1464 )
1466
1465
1467 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1466 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)
1467 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)
1468 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1470 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1469 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1471 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1470 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1472 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1471 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1473
1472
1474 @hybrid_property
1473 @hybrid_property
1475 def description_safe(self):
1474 def description_safe(self):
1476 from rhodecode.lib import helpers as h
1475 from rhodecode.lib import helpers as h
1477 return h.escape(self.description)
1476 return h.escape(self.description)
1478
1477
1479 @classmethod
1478 @classmethod
1480 def _get_ip_range(cls, ip_addr):
1479 def _get_ip_range(cls, ip_addr):
1481 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1480 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1482 return [str(net.network_address), str(net.broadcast_address)]
1481 return [str(net.network_address), str(net.broadcast_address)]
1483
1482
1484 def __json__(self):
1483 def __json__(self):
1485 return {
1484 return {
1486 'ip_addr': self.ip_addr,
1485 'ip_addr': self.ip_addr,
1487 'ip_range': self._get_ip_range(self.ip_addr),
1486 'ip_range': self._get_ip_range(self.ip_addr),
1488 }
1487 }
1489
1488
1490 def __repr__(self):
1489 def __repr__(self):
1491 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1490 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1492
1491
1493
1492
1494 class UserSshKeys(Base, BaseModel):
1493 class UserSshKeys(Base, BaseModel):
1495 __tablename__ = 'user_ssh_keys'
1494 __tablename__ = 'user_ssh_keys'
1496 __table_args__ = (
1495 __table_args__ = (
1497 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1496 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1498
1497
1499 UniqueConstraint('ssh_key_fingerprint'),
1498 UniqueConstraint('ssh_key_fingerprint'),
1500
1499
1501 base_table_args
1500 base_table_args
1502 )
1501 )
1503
1502
1504 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1503 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)
1504 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)
1505 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1507
1506
1508 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1507 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1509
1508
1510 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1509 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)
1510 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)
1511 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1513
1512
1514 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1513 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1515
1514
1516 def __json__(self):
1515 def __json__(self):
1517 data = {
1516 data = {
1518 'ssh_fingerprint': self.ssh_key_fingerprint,
1517 'ssh_fingerprint': self.ssh_key_fingerprint,
1519 'description': self.description,
1518 'description': self.description,
1520 'created_on': self.created_on
1519 'created_on': self.created_on
1521 }
1520 }
1522 return data
1521 return data
1523
1522
1524 def get_api_data(self):
1523 def get_api_data(self):
1525 data = self.__json__()
1524 data = self.__json__()
1526 return data
1525 return data
1527
1526
1528
1527
1529 class UserLog(Base, BaseModel):
1528 class UserLog(Base, BaseModel):
1530 __tablename__ = 'user_logs'
1529 __tablename__ = 'user_logs'
1531 __table_args__ = (
1530 __table_args__ = (
1532 base_table_args,
1531 base_table_args,
1533 )
1532 )
1534
1533
1535 VERSION_1 = 'v1'
1534 VERSION_1 = 'v1'
1536 VERSION_2 = 'v2'
1535 VERSION_2 = 'v2'
1537 VERSIONS = [VERSION_1, VERSION_2]
1536 VERSIONS = [VERSION_1, VERSION_2]
1538
1537
1539 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1538 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)
1539 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)
1540 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)
1541 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)
1542 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)
1543 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)
1544 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)
1545 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1547
1546
1548 version = Column("version", String(255), nullable=True, default=VERSION_1)
1547 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()))))
1548 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()))))
1549 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1551 user = relationship('User', cascade='', back_populates='user_log')
1550 user = relationship('User', cascade='', back_populates='user_log')
1552 repository = relationship('Repository', cascade='', back_populates='logs')
1551 repository = relationship('Repository', cascade='', back_populates='logs')
1553
1552
1554 def __repr__(self):
1553 def __repr__(self):
1555 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1554 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1556
1555
1557 def __json__(self):
1556 def __json__(self):
1558 return {
1557 return {
1559 'user_id': self.user_id,
1558 'user_id': self.user_id,
1560 'username': self.username,
1559 'username': self.username,
1561 'repository_id': self.repository_id,
1560 'repository_id': self.repository_id,
1562 'repository_name': self.repository_name,
1561 'repository_name': self.repository_name,
1563 'user_ip': self.user_ip,
1562 'user_ip': self.user_ip,
1564 'action_date': self.action_date,
1563 'action_date': self.action_date,
1565 'action': self.action,
1564 'action': self.action,
1566 }
1565 }
1567
1566
1568 @hybrid_property
1567 @hybrid_property
1569 def entry_id(self):
1568 def entry_id(self):
1570 return self.user_log_id
1569 return self.user_log_id
1571
1570
1572 @property
1571 @property
1573 def action_as_day(self):
1572 def action_as_day(self):
1574 return datetime.date(*self.action_date.timetuple()[:3])
1573 return datetime.date(*self.action_date.timetuple()[:3])
1575
1574
1576
1575
1577 class UserGroup(Base, BaseModel):
1576 class UserGroup(Base, BaseModel):
1578 __tablename__ = 'users_groups'
1577 __tablename__ = 'users_groups'
1579 __table_args__ = (
1578 __table_args__ = (
1580 base_table_args,
1579 base_table_args,
1581 )
1580 )
1582
1581
1583 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1582 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)
1583 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)
1584 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)
1585 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)
1586 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)
1587 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)
1588 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
1589 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1591
1590
1592 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1591 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')
1592 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')
1593 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')
1594 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')
1595 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1597
1596
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')
1597 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
1598
1600 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1599 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')
1600 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1602
1601
1603 @classmethod
1602 @classmethod
1604 def _load_group_data(cls, column):
1603 def _load_group_data(cls, column):
1605 if not column:
1604 if not column:
1606 return {}
1605 return {}
1607
1606
1608 try:
1607 try:
1609 return json.loads(column) or {}
1608 return json.loads(column) or {}
1610 except TypeError:
1609 except TypeError:
1611 return {}
1610 return {}
1612
1611
1613 @hybrid_property
1612 @hybrid_property
1614 def description_safe(self):
1613 def description_safe(self):
1615 from rhodecode.lib import helpers as h
1614 from rhodecode.lib import helpers as h
1616 return h.escape(self.user_group_description)
1615 return h.escape(self.user_group_description)
1617
1616
1618 @hybrid_property
1617 @hybrid_property
1619 def group_data(self):
1618 def group_data(self):
1620 return self._load_group_data(self._group_data)
1619 return self._load_group_data(self._group_data)
1621
1620
1622 @group_data.expression
1621 @group_data.expression
1623 def group_data(self, **kwargs):
1622 def group_data(self, **kwargs):
1624 return self._group_data
1623 return self._group_data
1625
1624
1626 @group_data.setter
1625 @group_data.setter
1627 def group_data(self, val):
1626 def group_data(self, val):
1628 try:
1627 try:
1629 self._group_data = json.dumps(val)
1628 self._group_data = json.dumps(val)
1630 except Exception:
1629 except Exception:
1631 log.error(traceback.format_exc())
1630 log.error(traceback.format_exc())
1632
1631
1633 @classmethod
1632 @classmethod
1634 def _load_sync(cls, group_data):
1633 def _load_sync(cls, group_data):
1635 if group_data:
1634 if group_data:
1636 return group_data.get('extern_type')
1635 return group_data.get('extern_type')
1637
1636
1638 @property
1637 @property
1639 def sync(self):
1638 def sync(self):
1640 return self._load_sync(self.group_data)
1639 return self._load_sync(self.group_data)
1641
1640
1642 def __repr__(self):
1641 def __repr__(self):
1643 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1642 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1644
1643
1645 @classmethod
1644 @classmethod
1646 def get_by_group_name(cls, group_name, cache=False,
1645 def get_by_group_name(cls, group_name, cache=False,
1647 case_insensitive=False):
1646 case_insensitive=False):
1648 if case_insensitive:
1647 if case_insensitive:
1649 q = cls.query().filter(func.lower(cls.users_group_name) ==
1648 q = cls.query().filter(func.lower(cls.users_group_name) ==
1650 func.lower(group_name))
1649 func.lower(group_name))
1651
1650
1652 else:
1651 else:
1653 q = cls.query().filter(cls.users_group_name == group_name)
1652 q = cls.query().filter(cls.users_group_name == group_name)
1654 if cache:
1653 if cache:
1655 name_key = _hash_key(group_name)
1654 name_key = _hash_key(group_name)
1656 q = q.options(
1655 q = q.options(
1657 FromCache("sql_cache_short", f"get_group_{name_key}"))
1656 FromCache("sql_cache_short", f"get_group_{name_key}"))
1658 return q.scalar()
1657 return q.scalar()
1659
1658
1660 @classmethod
1659 @classmethod
1661 def get(cls, user_group_id, cache=False):
1660 def get(cls, user_group_id, cache=False):
1662 if not user_group_id:
1661 if not user_group_id:
1663 return
1662 return
1664
1663
1665 user_group = cls.query()
1664 user_group = cls.query()
1666 if cache:
1665 if cache:
1667 user_group = user_group.options(
1666 user_group = user_group.options(
1668 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1667 FromCache("sql_cache_short", f"get_users_group_{user_group_id}"))
1669 return user_group.get(user_group_id)
1668 return user_group.get(user_group_id)
1670
1669
1671 def permissions(self, with_admins=True, with_owner=True,
1670 def permissions(self, with_admins=True, with_owner=True,
1672 expand_from_user_groups=False):
1671 expand_from_user_groups=False):
1673 """
1672 """
1674 Permissions for user groups
1673 Permissions for user groups
1675 """
1674 """
1676 _admin_perm = 'usergroup.admin'
1675 _admin_perm = 'usergroup.admin'
1677
1676
1678 owner_row = []
1677 owner_row = []
1679 if with_owner:
1678 if with_owner:
1680 usr = AttributeDict(self.user.get_dict())
1679 usr = AttributeDict(self.user.get_dict())
1681 usr.owner_row = True
1680 usr.owner_row = True
1682 usr.permission = _admin_perm
1681 usr.permission = _admin_perm
1683 owner_row.append(usr)
1682 owner_row.append(usr)
1684
1683
1685 super_admin_ids = []
1684 super_admin_ids = []
1686 super_admin_rows = []
1685 super_admin_rows = []
1687 if with_admins:
1686 if with_admins:
1688 for usr in User.get_all_super_admins():
1687 for usr in User.get_all_super_admins():
1689 super_admin_ids.append(usr.user_id)
1688 super_admin_ids.append(usr.user_id)
1690 # if this admin is also owner, don't double the record
1689 # if this admin is also owner, don't double the record
1691 if usr.user_id == owner_row[0].user_id:
1690 if usr.user_id == owner_row[0].user_id:
1692 owner_row[0].admin_row = True
1691 owner_row[0].admin_row = True
1693 else:
1692 else:
1694 usr = AttributeDict(usr.get_dict())
1693 usr = AttributeDict(usr.get_dict())
1695 usr.admin_row = True
1694 usr.admin_row = True
1696 usr.permission = _admin_perm
1695 usr.permission = _admin_perm
1697 super_admin_rows.append(usr)
1696 super_admin_rows.append(usr)
1698
1697
1699 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1698 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1700 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1699 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1701 joinedload(UserUserGroupToPerm.user),
1700 joinedload(UserUserGroupToPerm.user),
1702 joinedload(UserUserGroupToPerm.permission),)
1701 joinedload(UserUserGroupToPerm.permission),)
1703
1702
1704 # get owners and admins and permissions. We do a trick of re-writing
1703 # get owners and admins and permissions. We do a trick of re-writing
1705 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1704 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1706 # has a global reference and changing one object propagates to all
1705 # 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
1706 # others. This means if admin is also an owner admin_row that change
1708 # would propagate to both objects
1707 # would propagate to both objects
1709 perm_rows = []
1708 perm_rows = []
1710 for _usr in q.all():
1709 for _usr in q.all():
1711 usr = AttributeDict(_usr.user.get_dict())
1710 usr = AttributeDict(_usr.user.get_dict())
1712 # if this user is also owner/admin, mark as duplicate record
1711 # 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:
1712 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1714 usr.duplicate_perm = True
1713 usr.duplicate_perm = True
1715 usr.permission = _usr.permission.permission_name
1714 usr.permission = _usr.permission.permission_name
1716 perm_rows.append(usr)
1715 perm_rows.append(usr)
1717
1716
1718 # filter the perm rows by 'default' first and then sort them by
1717 # filter the perm rows by 'default' first and then sort them by
1719 # admin,write,read,none permissions sorted again alphabetically in
1718 # admin,write,read,none permissions sorted again alphabetically in
1720 # each group
1719 # each group
1721 perm_rows = sorted(perm_rows, key=display_user_sort)
1720 perm_rows = sorted(perm_rows, key=display_user_sort)
1722
1721
1723 user_groups_rows = []
1722 user_groups_rows = []
1724 if expand_from_user_groups:
1723 if expand_from_user_groups:
1725 for ug in self.permission_user_groups(with_members=True):
1724 for ug in self.permission_user_groups(with_members=True):
1726 for user_data in ug.members:
1725 for user_data in ug.members:
1727 user_groups_rows.append(user_data)
1726 user_groups_rows.append(user_data)
1728
1727
1729 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1728 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1730
1729
1731 def permission_user_groups(self, with_members=False):
1730 def permission_user_groups(self, with_members=False):
1732 q = UserGroupUserGroupToPerm.query()\
1731 q = UserGroupUserGroupToPerm.query()\
1733 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1732 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1734 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1733 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1735 joinedload(UserGroupUserGroupToPerm.target_user_group),
1734 joinedload(UserGroupUserGroupToPerm.target_user_group),
1736 joinedload(UserGroupUserGroupToPerm.permission),)
1735 joinedload(UserGroupUserGroupToPerm.permission),)
1737
1736
1738 perm_rows = []
1737 perm_rows = []
1739 for _user_group in q.all():
1738 for _user_group in q.all():
1740 entry = AttributeDict(_user_group.user_group.get_dict())
1739 entry = AttributeDict(_user_group.user_group.get_dict())
1741 entry.permission = _user_group.permission.permission_name
1740 entry.permission = _user_group.permission.permission_name
1742 if with_members:
1741 if with_members:
1743 entry.members = [x.user.get_dict()
1742 entry.members = [x.user.get_dict()
1744 for x in _user_group.user_group.members]
1743 for x in _user_group.user_group.members]
1745 perm_rows.append(entry)
1744 perm_rows.append(entry)
1746
1745
1747 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1746 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1748 return perm_rows
1747 return perm_rows
1749
1748
1750 def _get_default_perms(self, user_group, suffix=''):
1749 def _get_default_perms(self, user_group, suffix=''):
1751 from rhodecode.model.permission import PermissionModel
1750 from rhodecode.model.permission import PermissionModel
1752 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1751 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1753
1752
1754 def get_default_perms(self, suffix=''):
1753 def get_default_perms(self, suffix=''):
1755 return self._get_default_perms(self, suffix)
1754 return self._get_default_perms(self, suffix)
1756
1755
1757 def get_api_data(self, with_group_members=True, include_secrets=False):
1756 def get_api_data(self, with_group_members=True, include_secrets=False):
1758 """
1757 """
1759 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1758 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1760 basically forwarded.
1759 basically forwarded.
1761
1760
1762 """
1761 """
1763 user_group = self
1762 user_group = self
1764 data = {
1763 data = {
1765 'users_group_id': user_group.users_group_id,
1764 'users_group_id': user_group.users_group_id,
1766 'group_name': user_group.users_group_name,
1765 'group_name': user_group.users_group_name,
1767 'group_description': user_group.user_group_description,
1766 'group_description': user_group.user_group_description,
1768 'active': user_group.users_group_active,
1767 'active': user_group.users_group_active,
1769 'owner': user_group.user.username,
1768 'owner': user_group.user.username,
1770 'sync': user_group.sync,
1769 'sync': user_group.sync,
1771 'owner_email': user_group.user.email,
1770 'owner_email': user_group.user.email,
1772 }
1771 }
1773
1772
1774 if with_group_members:
1773 if with_group_members:
1775 users = []
1774 users = []
1776 for user in user_group.members:
1775 for user in user_group.members:
1777 user = user.user
1776 user = user.user
1778 users.append(user.get_api_data(include_secrets=include_secrets))
1777 users.append(user.get_api_data(include_secrets=include_secrets))
1779 data['users'] = users
1778 data['users'] = users
1780
1779
1781 return data
1780 return data
1782
1781
1783
1782
1784 class UserGroupMember(Base, BaseModel):
1783 class UserGroupMember(Base, BaseModel):
1785 __tablename__ = 'users_groups_members'
1784 __tablename__ = 'users_groups_members'
1786 __table_args__ = (
1785 __table_args__ = (
1787 base_table_args,
1786 base_table_args,
1788 )
1787 )
1789
1788
1790 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1789 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)
1790 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)
1791 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1793
1792
1794 user = relationship('User', lazy='joined', back_populates='group_member')
1793 user = relationship('User', lazy='joined', back_populates='group_member')
1795 users_group = relationship('UserGroup', back_populates='members')
1794 users_group = relationship('UserGroup', back_populates='members')
1796
1795
1797 def __init__(self, gr_id='', u_id=''):
1796 def __init__(self, gr_id='', u_id=''):
1798 self.users_group_id = gr_id
1797 self.users_group_id = gr_id
1799 self.user_id = u_id
1798 self.user_id = u_id
1800
1799
1801
1800
1802 class RepositoryField(Base, BaseModel):
1801 class RepositoryField(Base, BaseModel):
1803 __tablename__ = 'repositories_fields'
1802 __tablename__ = 'repositories_fields'
1804 __table_args__ = (
1803 __table_args__ = (
1805 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1804 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1806 base_table_args,
1805 base_table_args,
1807 )
1806 )
1808
1807
1809 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1808 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1810
1809
1811 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1810 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)
1811 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1813 field_key = Column("field_key", String(250))
1812 field_key = Column("field_key", String(250))
1814 field_label = Column("field_label", String(1024), nullable=False)
1813 field_label = Column("field_label", String(1024), nullable=False)
1815 field_value = Column("field_value", String(10000), nullable=False)
1814 field_value = Column("field_value", String(10000), nullable=False)
1816 field_desc = Column("field_desc", String(1024), nullable=False)
1815 field_desc = Column("field_desc", String(1024), nullable=False)
1817 field_type = Column("field_type", String(255), nullable=False, unique=None)
1816 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)
1817 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1819
1818
1820 repository = relationship('Repository', back_populates='extra_fields')
1819 repository = relationship('Repository', back_populates='extra_fields')
1821
1820
1822 @property
1821 @property
1823 def field_key_prefixed(self):
1822 def field_key_prefixed(self):
1824 return 'ex_%s' % self.field_key
1823 return 'ex_%s' % self.field_key
1825
1824
1826 @classmethod
1825 @classmethod
1827 def un_prefix_key(cls, key):
1826 def un_prefix_key(cls, key):
1828 if key.startswith(cls.PREFIX):
1827 if key.startswith(cls.PREFIX):
1829 return key[len(cls.PREFIX):]
1828 return key[len(cls.PREFIX):]
1830 return key
1829 return key
1831
1830
1832 @classmethod
1831 @classmethod
1833 def get_by_key_name(cls, key, repo):
1832 def get_by_key_name(cls, key, repo):
1834 row = cls.query()\
1833 row = cls.query()\
1835 .filter(cls.repository == repo)\
1834 .filter(cls.repository == repo)\
1836 .filter(cls.field_key == key).scalar()
1835 .filter(cls.field_key == key).scalar()
1837 return row
1836 return row
1838
1837
1839
1838
1840 class Repository(Base, BaseModel):
1839 class Repository(Base, BaseModel):
1841 __tablename__ = 'repositories'
1840 __tablename__ = 'repositories'
1842 __table_args__ = (
1841 __table_args__ = (
1843 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1842 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1844 base_table_args,
1843 base_table_args,
1845 )
1844 )
1846 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1845 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1847 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1846 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1848 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1847 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1849
1848
1850 STATE_CREATED = 'repo_state_created'
1849 STATE_CREATED = 'repo_state_created'
1851 STATE_PENDING = 'repo_state_pending'
1850 STATE_PENDING = 'repo_state_pending'
1852 STATE_ERROR = 'repo_state_error'
1851 STATE_ERROR = 'repo_state_error'
1853
1852
1854 LOCK_AUTOMATIC = 'lock_auto'
1853 LOCK_AUTOMATIC = 'lock_auto'
1855 LOCK_API = 'lock_api'
1854 LOCK_API = 'lock_api'
1856 LOCK_WEB = 'lock_web'
1855 LOCK_WEB = 'lock_web'
1857 LOCK_PULL = 'lock_pull'
1856 LOCK_PULL = 'lock_pull'
1858
1857
1859 NAME_SEP = URL_SEP
1858 NAME_SEP = URL_SEP
1860
1859
1861 repo_id = Column(
1860 repo_id = Column(
1862 "repo_id", Integer(), nullable=False, unique=True, default=None,
1861 "repo_id", Integer(), nullable=False, unique=True, default=None,
1863 primary_key=True)
1862 primary_key=True)
1864 _repo_name = Column(
1863 _repo_name = Column(
1865 "repo_name", Text(), nullable=False, default=None)
1864 "repo_name", Text(), nullable=False, default=None)
1866 repo_name_hash = Column(
1865 repo_name_hash = Column(
1867 "repo_name_hash", String(255), nullable=False, unique=True)
1866 "repo_name_hash", String(255), nullable=False, unique=True)
1868 repo_state = Column("repo_state", String(255), nullable=True)
1867 repo_state = Column("repo_state", String(255), nullable=True)
1869
1868
1870 clone_uri = Column(
1869 clone_uri = Column(
1871 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1870 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1872 default=None)
1871 default=None)
1873 push_uri = Column(
1872 push_uri = Column(
1874 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1873 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1875 default=None)
1874 default=None)
1876 repo_type = Column(
1875 repo_type = Column(
1877 "repo_type", String(255), nullable=False, unique=False, default=None)
1876 "repo_type", String(255), nullable=False, unique=False, default=None)
1878 user_id = Column(
1877 user_id = Column(
1879 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1878 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1880 unique=False, default=None)
1879 unique=False, default=None)
1881 private = Column(
1880 private = Column(
1882 "private", Boolean(), nullable=True, unique=None, default=None)
1881 "private", Boolean(), nullable=True, unique=None, default=None)
1883 archived = Column(
1882 archived = Column(
1884 "archived", Boolean(), nullable=True, unique=None, default=None)
1883 "archived", Boolean(), nullable=True, unique=None, default=None)
1885 enable_statistics = Column(
1884 enable_statistics = Column(
1886 "statistics", Boolean(), nullable=True, unique=None, default=True)
1885 "statistics", Boolean(), nullable=True, unique=None, default=True)
1887 enable_downloads = Column(
1886 enable_downloads = Column(
1888 "downloads", Boolean(), nullable=True, unique=None, default=True)
1887 "downloads", Boolean(), nullable=True, unique=None, default=True)
1889 description = Column(
1888 description = Column(
1890 "description", String(10000), nullable=True, unique=None, default=None)
1889 "description", String(10000), nullable=True, unique=None, default=None)
1891 created_on = Column(
1890 created_on = Column(
1892 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1891 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1893 default=datetime.datetime.now)
1892 default=datetime.datetime.now)
1894 updated_on = Column(
1893 updated_on = Column(
1895 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1894 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1896 default=datetime.datetime.now)
1895 default=datetime.datetime.now)
1897 _landing_revision = Column(
1896 _landing_revision = Column(
1898 "landing_revision", String(255), nullable=False, unique=False,
1897 "landing_revision", String(255), nullable=False, unique=False,
1899 default=None)
1898 default=None)
1900 enable_locking = Column(
1899 enable_locking = Column(
1901 "enable_locking", Boolean(), nullable=False, unique=None,
1900 "enable_locking", Boolean(), nullable=False, unique=None,
1902 default=False)
1901 default=False)
1903 _locked = Column(
1902 _locked = Column(
1904 "locked", String(255), nullable=True, unique=False, default=None)
1903 "locked", String(255), nullable=True, unique=False, default=None)
1905 _changeset_cache = Column(
1904 _changeset_cache = Column(
1906 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1905 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1907
1906
1908 fork_id = Column(
1907 fork_id = Column(
1909 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1908 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1910 nullable=True, unique=False, default=None)
1909 nullable=True, unique=False, default=None)
1911 group_id = Column(
1910 group_id = Column(
1912 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1911 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1913 unique=False, default=None)
1912 unique=False, default=None)
1914
1913
1915 user = relationship('User', lazy='joined', back_populates='repositories')
1914 user = relationship('User', lazy='joined', back_populates='repositories')
1916 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1915 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1917 group = relationship('RepoGroup', lazy='joined')
1916 group = relationship('RepoGroup', lazy='joined')
1918 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1917 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')
1918 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1920 stats = relationship('Statistics', cascade='all', uselist=False)
1919 stats = relationship('Statistics', cascade='all', uselist=False)
1921
1920
1922 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1921 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')
1922 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1924
1923
1925 logs = relationship('UserLog', back_populates='repository')
1924 logs = relationship('UserLog', back_populates='repository')
1926
1925
1927 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1926 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1928
1927
1929 pull_requests_source = relationship(
1928 pull_requests_source = relationship(
1930 'PullRequest',
1929 'PullRequest',
1931 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1930 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1932 cascade="all, delete-orphan",
1931 cascade="all, delete-orphan",
1933 overlaps="source_repo"
1932 overlaps="source_repo"
1934 )
1933 )
1935 pull_requests_target = relationship(
1934 pull_requests_target = relationship(
1936 'PullRequest',
1935 'PullRequest',
1937 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1936 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1938 cascade="all, delete-orphan",
1937 cascade="all, delete-orphan",
1939 overlaps="target_repo"
1938 overlaps="target_repo"
1940 )
1939 )
1941
1940
1942 ui = relationship('RepoRhodeCodeUi', cascade="all")
1941 ui = relationship('RepoRhodeCodeUi', cascade="all")
1943 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1942 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1944 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1943 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1945
1944
1946 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1945 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1947
1946
1948 # no cascade, set NULL
1947 # no cascade, set NULL
1949 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1948 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1950
1949
1951 review_rules = relationship('RepoReviewRule')
1950 review_rules = relationship('RepoReviewRule')
1952 user_branch_perms = relationship('UserToRepoBranchPermission')
1951 user_branch_perms = relationship('UserToRepoBranchPermission')
1953 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1952 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1954
1953
1955 def __repr__(self):
1954 def __repr__(self):
1956 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1955 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1957
1956
1958 @hybrid_property
1957 @hybrid_property
1959 def description_safe(self):
1958 def description_safe(self):
1960 from rhodecode.lib import helpers as h
1959 from rhodecode.lib import helpers as h
1961 return h.escape(self.description)
1960 return h.escape(self.description)
1962
1961
1963 @hybrid_property
1962 @hybrid_property
1964 def landing_rev(self):
1963 def landing_rev(self):
1965 # always should return [rev_type, rev], e.g ['branch', 'master']
1964 # always should return [rev_type, rev], e.g ['branch', 'master']
1966 if self._landing_revision:
1965 if self._landing_revision:
1967 _rev_info = self._landing_revision.split(':')
1966 _rev_info = self._landing_revision.split(':')
1968 if len(_rev_info) < 2:
1967 if len(_rev_info) < 2:
1969 _rev_info.insert(0, 'rev')
1968 _rev_info.insert(0, 'rev')
1970 return [_rev_info[0], _rev_info[1]]
1969 return [_rev_info[0], _rev_info[1]]
1971 return [None, None]
1970 return [None, None]
1972
1971
1973 @property
1972 @property
1974 def landing_ref_type(self):
1973 def landing_ref_type(self):
1975 return self.landing_rev[0]
1974 return self.landing_rev[0]
1976
1975
1977 @property
1976 @property
1978 def landing_ref_name(self):
1977 def landing_ref_name(self):
1979 return self.landing_rev[1]
1978 return self.landing_rev[1]
1980
1979
1981 @landing_rev.setter
1980 @landing_rev.setter
1982 def landing_rev(self, val):
1981 def landing_rev(self, val):
1983 if ':' not in val:
1982 if ':' not in val:
1984 raise ValueError('value must be delimited with `:` and consist '
1983 raise ValueError('value must be delimited with `:` and consist '
1985 'of <rev_type>:<rev>, got %s instead' % val)
1984 'of <rev_type>:<rev>, got %s instead' % val)
1986 self._landing_revision = val
1985 self._landing_revision = val
1987
1986
1988 @hybrid_property
1987 @hybrid_property
1989 def locked(self):
1988 def locked(self):
1990 if self._locked:
1989 if self._locked:
1991 user_id, timelocked, reason = self._locked.split(':')
1990 user_id, timelocked, reason = self._locked.split(':')
1992 lock_values = int(user_id), timelocked, reason
1991 lock_values = int(user_id), timelocked, reason
1993 else:
1992 else:
1994 lock_values = [None, None, None]
1993 lock_values = [None, None, None]
1995 return lock_values
1994 return lock_values
1996
1995
1997 @locked.setter
1996 @locked.setter
1998 def locked(self, val):
1997 def locked(self, val):
1999 if val and isinstance(val, (list, tuple)):
1998 if val and isinstance(val, (list, tuple)):
2000 self._locked = ':'.join(map(str, val))
1999 self._locked = ':'.join(map(str, val))
2001 else:
2000 else:
2002 self._locked = None
2001 self._locked = None
2003
2002
2004 @classmethod
2003 @classmethod
2005 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2004 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2006 from rhodecode.lib.vcs.backends.base import EmptyCommit
2005 from rhodecode.lib.vcs.backends.base import EmptyCommit
2007 dummy = EmptyCommit().__json__()
2006 dummy = EmptyCommit().__json__()
2008 if not changeset_cache_raw:
2007 if not changeset_cache_raw:
2009 dummy['source_repo_id'] = repo_id
2008 dummy['source_repo_id'] = repo_id
2010 return json.loads(json.dumps(dummy))
2009 return json.loads(json.dumps(dummy))
2011
2010
2012 try:
2011 try:
2013 return json.loads(changeset_cache_raw)
2012 return json.loads(changeset_cache_raw)
2014 except TypeError:
2013 except TypeError:
2015 return dummy
2014 return dummy
2016 except Exception:
2015 except Exception:
2017 log.error(traceback.format_exc())
2016 log.error(traceback.format_exc())
2018 return dummy
2017 return dummy
2019
2018
2020 @hybrid_property
2019 @hybrid_property
2021 def changeset_cache(self):
2020 def changeset_cache(self):
2022 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2021 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
2023
2022
2024 @changeset_cache.setter
2023 @changeset_cache.setter
2025 def changeset_cache(self, val):
2024 def changeset_cache(self, val):
2026 try:
2025 try:
2027 self._changeset_cache = json.dumps(val)
2026 self._changeset_cache = json.dumps(val)
2028 except Exception:
2027 except Exception:
2029 log.error(traceback.format_exc())
2028 log.error(traceback.format_exc())
2030
2029
2031 @hybrid_property
2030 @hybrid_property
2032 def repo_name(self):
2031 def repo_name(self):
2033 return self._repo_name
2032 return self._repo_name
2034
2033
2035 @repo_name.setter
2034 @repo_name.setter
2036 def repo_name(self, value):
2035 def repo_name(self, value):
2037 self._repo_name = value
2036 self._repo_name = value
2038 self.repo_name_hash = sha1(safe_bytes(value))
2037 self.repo_name_hash = sha1(safe_bytes(value))
2039
2038
2040 @classmethod
2039 @classmethod
2041 def normalize_repo_name(cls, repo_name):
2040 def normalize_repo_name(cls, repo_name):
2042 """
2041 """
2043 Normalizes os specific repo_name to the format internally stored inside
2042 Normalizes os specific repo_name to the format internally stored inside
2044 database using URL_SEP
2043 database using URL_SEP
2045
2044
2046 :param cls:
2045 :param cls:
2047 :param repo_name:
2046 :param repo_name:
2048 """
2047 """
2049 return cls.NAME_SEP.join(repo_name.split(os.sep))
2048 return cls.NAME_SEP.join(repo_name.split(os.sep))
2050
2049
2051 @classmethod
2050 @classmethod
2052 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2051 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
2053 session = Session()
2052 session = Session()
2054 q = session.query(cls).filter(cls.repo_name == repo_name)
2053 q = session.query(cls).filter(cls.repo_name == repo_name)
2055
2054
2056 if cache:
2055 if cache:
2057 if identity_cache:
2056 if identity_cache:
2058 val = cls.identity_cache(session, 'repo_name', repo_name)
2057 val = cls.identity_cache(session, 'repo_name', repo_name)
2059 if val:
2058 if val:
2060 return val
2059 return val
2061 else:
2060 else:
2062 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2061 cache_key = f"get_repo_by_name_{_hash_key(repo_name)}"
2063 q = q.options(
2062 q = q.options(
2064 FromCache("sql_cache_short", cache_key))
2063 FromCache("sql_cache_short", cache_key))
2065
2064
2066 return q.scalar()
2065 return q.scalar()
2067
2066
2068 @classmethod
2067 @classmethod
2069 def get_by_id_or_repo_name(cls, repoid):
2068 def get_by_id_or_repo_name(cls, repoid):
2070 if isinstance(repoid, int):
2069 if isinstance(repoid, int):
2071 try:
2070 try:
2072 repo = cls.get(repoid)
2071 repo = cls.get(repoid)
2073 except ValueError:
2072 except ValueError:
2074 repo = None
2073 repo = None
2075 else:
2074 else:
2076 repo = cls.get_by_repo_name(repoid)
2075 repo = cls.get_by_repo_name(repoid)
2077 return repo
2076 return repo
2078
2077
2079 @classmethod
2078 @classmethod
2080 def get_by_full_path(cls, repo_full_path):
2079 def get_by_full_path(cls, repo_full_path):
2081 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2080 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
2082 repo_name = cls.normalize_repo_name(repo_name)
2081 repo_name = cls.normalize_repo_name(repo_name)
2083 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2082 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
2084
2083
2085 @classmethod
2084 @classmethod
2086 def get_repo_forks(cls, repo_id):
2085 def get_repo_forks(cls, repo_id):
2087 return cls.query().filter(Repository.fork_id == repo_id)
2086 return cls.query().filter(Repository.fork_id == repo_id)
2088
2087
2089 @classmethod
2088 @classmethod
2090 def base_path(cls):
2089 def base_path(cls):
2091 """
2090 """
2092 Returns base path when all repos are stored
2091 Returns base path when all repos are stored
2093
2092
2094 :param cls:
2093 :param cls:
2095 """
2094 """
2096 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2095 from rhodecode.lib.utils import get_rhodecode_repo_store_path
2097 return get_rhodecode_repo_store_path()
2096 return get_rhodecode_repo_store_path()
2098
2097
2099 @classmethod
2098 @classmethod
2100 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2099 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
2101 case_insensitive=True, archived=False):
2100 case_insensitive=True, archived=False):
2102 q = Repository.query()
2101 q = Repository.query()
2103
2102
2104 if not archived:
2103 if not archived:
2105 q = q.filter(Repository.archived.isnot(true()))
2104 q = q.filter(Repository.archived.isnot(true()))
2106
2105
2107 if not isinstance(user_id, Optional):
2106 if not isinstance(user_id, Optional):
2108 q = q.filter(Repository.user_id == user_id)
2107 q = q.filter(Repository.user_id == user_id)
2109
2108
2110 if not isinstance(group_id, Optional):
2109 if not isinstance(group_id, Optional):
2111 q = q.filter(Repository.group_id == group_id)
2110 q = q.filter(Repository.group_id == group_id)
2112
2111
2113 if case_insensitive:
2112 if case_insensitive:
2114 q = q.order_by(func.lower(Repository.repo_name))
2113 q = q.order_by(func.lower(Repository.repo_name))
2115 else:
2114 else:
2116 q = q.order_by(Repository.repo_name)
2115 q = q.order_by(Repository.repo_name)
2117
2116
2118 return q.all()
2117 return q.all()
2119
2118
2120 @property
2119 @property
2121 def repo_uid(self):
2120 def repo_uid(self):
2122 return '_{}'.format(self.repo_id)
2121 return '_{}'.format(self.repo_id)
2123
2122
2124 @property
2123 @property
2125 def forks(self):
2124 def forks(self):
2126 """
2125 """
2127 Return forks of this repo
2126 Return forks of this repo
2128 """
2127 """
2129 return Repository.get_repo_forks(self.repo_id)
2128 return Repository.get_repo_forks(self.repo_id)
2130
2129
2131 @property
2130 @property
2132 def parent(self):
2131 def parent(self):
2133 """
2132 """
2134 Returns fork parent
2133 Returns fork parent
2135 """
2134 """
2136 return self.fork
2135 return self.fork
2137
2136
2138 @property
2137 @property
2139 def just_name(self):
2138 def just_name(self):
2140 return self.repo_name.split(self.NAME_SEP)[-1]
2139 return self.repo_name.split(self.NAME_SEP)[-1]
2141
2140
2142 @property
2141 @property
2143 def groups_with_parents(self):
2142 def groups_with_parents(self):
2144 groups = []
2143 groups = []
2145 if self.group is None:
2144 if self.group is None:
2146 return groups
2145 return groups
2147
2146
2148 cur_gr = self.group
2147 cur_gr = self.group
2149 groups.insert(0, cur_gr)
2148 groups.insert(0, cur_gr)
2150 while 1:
2149 while 1:
2151 gr = getattr(cur_gr, 'parent_group', None)
2150 gr = getattr(cur_gr, 'parent_group', None)
2152 cur_gr = cur_gr.parent_group
2151 cur_gr = cur_gr.parent_group
2153 if gr is None:
2152 if gr is None:
2154 break
2153 break
2155 groups.insert(0, gr)
2154 groups.insert(0, gr)
2156
2155
2157 return groups
2156 return groups
2158
2157
2159 @property
2158 @property
2160 def groups_and_repo(self):
2159 def groups_and_repo(self):
2161 return self.groups_with_parents, self
2160 return self.groups_with_parents, self
2162
2161
2163 @property
2162 @property
2164 def repo_path(self):
2163 def repo_path(self):
2165 """
2164 """
2166 Returns base full path for that repository means where it actually
2165 Returns base full path for that repository means where it actually
2167 exists on a filesystem
2166 exists on a filesystem
2168 """
2167 """
2169 return self.base_path()
2168 return self.base_path()
2170
2169
2171 @property
2170 @property
2172 def repo_full_path(self):
2171 def repo_full_path(self):
2173 p = [self.repo_path]
2172 p = [self.repo_path]
2174 # we need to split the name by / since this is how we store the
2173 # 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
2174 # names in the database, but that eventually needs to be converted
2176 # into a valid system path
2175 # into a valid system path
2177 p += self.repo_name.split(self.NAME_SEP)
2176 p += self.repo_name.split(self.NAME_SEP)
2178 return os.path.join(*map(safe_str, p))
2177 return os.path.join(*map(safe_str, p))
2179
2178
2180 @property
2179 @property
2181 def cache_keys(self):
2180 def cache_keys(self):
2182 """
2181 """
2183 Returns associated cache keys for that repo
2182 Returns associated cache keys for that repo
2184 """
2183 """
2185 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2184 repo_namespace_key = CacheKey.REPO_INVALIDATION_NAMESPACE.format(repo_id=self.repo_id)
2186 return CacheKey.query()\
2185 return CacheKey.query()\
2187 .filter(CacheKey.cache_key == repo_namespace_key)\
2186 .filter(CacheKey.cache_key == repo_namespace_key)\
2188 .order_by(CacheKey.cache_key)\
2187 .order_by(CacheKey.cache_key)\
2189 .all()
2188 .all()
2190
2189
2191 @property
2190 @property
2192 def cached_diffs_relative_dir(self):
2191 def cached_diffs_relative_dir(self):
2193 """
2192 """
2194 Return a relative to the repository store path of cached diffs
2193 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
2194 used for safe display for users, who shouldn't know the absolute store
2196 path
2195 path
2197 """
2196 """
2198 return os.path.join(
2197 return os.path.join(
2199 os.path.dirname(self.repo_name),
2198 os.path.dirname(self.repo_name),
2200 self.cached_diffs_dir.split(os.path.sep)[-1])
2199 self.cached_diffs_dir.split(os.path.sep)[-1])
2201
2200
2202 @property
2201 @property
2203 def cached_diffs_dir(self):
2202 def cached_diffs_dir(self):
2204 path = self.repo_full_path
2203 path = self.repo_full_path
2205 return os.path.join(
2204 return os.path.join(
2206 os.path.dirname(path),
2205 os.path.dirname(path),
2207 f'.__shadow_diff_cache_repo_{self.repo_id}')
2206 f'.__shadow_diff_cache_repo_{self.repo_id}')
2208
2207
2209 def cached_diffs(self):
2208 def cached_diffs(self):
2210 diff_cache_dir = self.cached_diffs_dir
2209 diff_cache_dir = self.cached_diffs_dir
2211 if os.path.isdir(diff_cache_dir):
2210 if os.path.isdir(diff_cache_dir):
2212 return os.listdir(diff_cache_dir)
2211 return os.listdir(diff_cache_dir)
2213 return []
2212 return []
2214
2213
2215 def shadow_repos(self):
2214 def shadow_repos(self):
2216 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2215 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2217 return [
2216 return [
2218 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2217 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2219 if x.startswith(shadow_repos_pattern)
2218 if x.startswith(shadow_repos_pattern)
2220 ]
2219 ]
2221
2220
2222 def get_new_name(self, repo_name):
2221 def get_new_name(self, repo_name):
2223 """
2222 """
2224 returns new full repository name based on assigned group and new new
2223 returns new full repository name based on assigned group and new new
2225
2224
2226 :param repo_name:
2225 :param repo_name:
2227 """
2226 """
2228 path_prefix = self.group.full_path_splitted if self.group else []
2227 path_prefix = self.group.full_path_splitted if self.group else []
2229 return self.NAME_SEP.join(path_prefix + [repo_name])
2228 return self.NAME_SEP.join(path_prefix + [repo_name])
2230
2229
2231 @property
2230 @property
2232 def _config(self):
2231 def _config(self):
2233 """
2232 """
2234 Returns db based config object.
2233 Returns db based config object.
2235 """
2234 """
2236 from rhodecode.lib.utils import make_db_config
2235 from rhodecode.lib.utils import make_db_config
2237 return make_db_config(clear_session=False, repo=self)
2236 return make_db_config(clear_session=False, repo=self)
2238
2237
2239 def permissions(self, with_admins=True, with_owner=True,
2238 def permissions(self, with_admins=True, with_owner=True,
2240 expand_from_user_groups=False):
2239 expand_from_user_groups=False):
2241 """
2240 """
2242 Permissions for repositories
2241 Permissions for repositories
2243 """
2242 """
2244 _admin_perm = 'repository.admin'
2243 _admin_perm = 'repository.admin'
2245
2244
2246 owner_row = []
2245 owner_row = []
2247 if with_owner:
2246 if with_owner:
2248 usr = AttributeDict(self.user.get_dict())
2247 usr = AttributeDict(self.user.get_dict())
2249 usr.owner_row = True
2248 usr.owner_row = True
2250 usr.permission = _admin_perm
2249 usr.permission = _admin_perm
2251 usr.permission_id = None
2250 usr.permission_id = None
2252 owner_row.append(usr)
2251 owner_row.append(usr)
2253
2252
2254 super_admin_ids = []
2253 super_admin_ids = []
2255 super_admin_rows = []
2254 super_admin_rows = []
2256 if with_admins:
2255 if with_admins:
2257 for usr in User.get_all_super_admins():
2256 for usr in User.get_all_super_admins():
2258 super_admin_ids.append(usr.user_id)
2257 super_admin_ids.append(usr.user_id)
2259 # if this admin is also owner, don't double the record
2258 # if this admin is also owner, don't double the record
2260 if usr.user_id == owner_row[0].user_id:
2259 if usr.user_id == owner_row[0].user_id:
2261 owner_row[0].admin_row = True
2260 owner_row[0].admin_row = True
2262 else:
2261 else:
2263 usr = AttributeDict(usr.get_dict())
2262 usr = AttributeDict(usr.get_dict())
2264 usr.admin_row = True
2263 usr.admin_row = True
2265 usr.permission = _admin_perm
2264 usr.permission = _admin_perm
2266 usr.permission_id = None
2265 usr.permission_id = None
2267 super_admin_rows.append(usr)
2266 super_admin_rows.append(usr)
2268
2267
2269 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2268 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2270 q = q.options(joinedload(UserRepoToPerm.repository),
2269 q = q.options(joinedload(UserRepoToPerm.repository),
2271 joinedload(UserRepoToPerm.user),
2270 joinedload(UserRepoToPerm.user),
2272 joinedload(UserRepoToPerm.permission),)
2271 joinedload(UserRepoToPerm.permission),)
2273
2272
2274 # get owners and admins and permissions. We do a trick of re-writing
2273 # get owners and admins and permissions. We do a trick of re-writing
2275 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2274 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2276 # has a global reference and changing one object propagates to all
2275 # 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
2276 # others. This means if admin is also an owner admin_row that change
2278 # would propagate to both objects
2277 # would propagate to both objects
2279 perm_rows = []
2278 perm_rows = []
2280 for _usr in q.all():
2279 for _usr in q.all():
2281 usr = AttributeDict(_usr.user.get_dict())
2280 usr = AttributeDict(_usr.user.get_dict())
2282 # if this user is also owner/admin, mark as duplicate record
2281 # 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:
2282 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2284 usr.duplicate_perm = True
2283 usr.duplicate_perm = True
2285 # also check if this permission is maybe used by branch_permissions
2284 # also check if this permission is maybe used by branch_permissions
2286 if _usr.branch_perm_entry:
2285 if _usr.branch_perm_entry:
2287 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2286 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2288
2287
2289 usr.permission = _usr.permission.permission_name
2288 usr.permission = _usr.permission.permission_name
2290 usr.permission_id = _usr.repo_to_perm_id
2289 usr.permission_id = _usr.repo_to_perm_id
2291 perm_rows.append(usr)
2290 perm_rows.append(usr)
2292
2291
2293 # filter the perm rows by 'default' first and then sort them by
2292 # filter the perm rows by 'default' first and then sort them by
2294 # admin,write,read,none permissions sorted again alphabetically in
2293 # admin,write,read,none permissions sorted again alphabetically in
2295 # each group
2294 # each group
2296 perm_rows = sorted(perm_rows, key=display_user_sort)
2295 perm_rows = sorted(perm_rows, key=display_user_sort)
2297
2296
2298 user_groups_rows = []
2297 user_groups_rows = []
2299 if expand_from_user_groups:
2298 if expand_from_user_groups:
2300 for ug in self.permission_user_groups(with_members=True):
2299 for ug in self.permission_user_groups(with_members=True):
2301 for user_data in ug.members:
2300 for user_data in ug.members:
2302 user_groups_rows.append(user_data)
2301 user_groups_rows.append(user_data)
2303
2302
2304 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2303 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2305
2304
2306 def permission_user_groups(self, with_members=True):
2305 def permission_user_groups(self, with_members=True):
2307 q = UserGroupRepoToPerm.query()\
2306 q = UserGroupRepoToPerm.query()\
2308 .filter(UserGroupRepoToPerm.repository == self)
2307 .filter(UserGroupRepoToPerm.repository == self)
2309 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2308 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2310 joinedload(UserGroupRepoToPerm.users_group),
2309 joinedload(UserGroupRepoToPerm.users_group),
2311 joinedload(UserGroupRepoToPerm.permission),)
2310 joinedload(UserGroupRepoToPerm.permission),)
2312
2311
2313 perm_rows = []
2312 perm_rows = []
2314 for _user_group in q.all():
2313 for _user_group in q.all():
2315 entry = AttributeDict(_user_group.users_group.get_dict())
2314 entry = AttributeDict(_user_group.users_group.get_dict())
2316 entry.permission = _user_group.permission.permission_name
2315 entry.permission = _user_group.permission.permission_name
2317 if with_members:
2316 if with_members:
2318 entry.members = [x.user.get_dict()
2317 entry.members = [x.user.get_dict()
2319 for x in _user_group.users_group.members]
2318 for x in _user_group.users_group.members]
2320 perm_rows.append(entry)
2319 perm_rows.append(entry)
2321
2320
2322 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2321 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2323 return perm_rows
2322 return perm_rows
2324
2323
2325 def get_api_data(self, include_secrets=False):
2324 def get_api_data(self, include_secrets=False):
2326 """
2325 """
2327 Common function for generating repo api data
2326 Common function for generating repo api data
2328
2327
2329 :param include_secrets: See :meth:`User.get_api_data`.
2328 :param include_secrets: See :meth:`User.get_api_data`.
2330
2329
2331 """
2330 """
2332 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2331 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2333 # move this methods on models level.
2332 # move this methods on models level.
2334 from rhodecode.model.settings import SettingsModel
2333 from rhodecode.model.settings import SettingsModel
2335 from rhodecode.model.repo import RepoModel
2334 from rhodecode.model.repo import RepoModel
2336
2335
2337 repo = self
2336 repo = self
2338 _user_id, _time, _reason = self.locked
2337 _user_id, _time, _reason = self.locked
2339
2338
2340 data = {
2339 data = {
2341 'repo_id': repo.repo_id,
2340 'repo_id': repo.repo_id,
2342 'repo_name': repo.repo_name,
2341 'repo_name': repo.repo_name,
2343 'repo_type': repo.repo_type,
2342 'repo_type': repo.repo_type,
2344 'clone_uri': repo.clone_uri or '',
2343 'clone_uri': repo.clone_uri or '',
2345 'push_uri': repo.push_uri or '',
2344 'push_uri': repo.push_uri or '',
2346 'url': RepoModel().get_url(self),
2345 'url': RepoModel().get_url(self),
2347 'private': repo.private,
2346 'private': repo.private,
2348 'created_on': repo.created_on,
2347 'created_on': repo.created_on,
2349 'description': repo.description_safe,
2348 'description': repo.description_safe,
2350 'landing_rev': repo.landing_rev,
2349 'landing_rev': repo.landing_rev,
2351 'owner': repo.user.username,
2350 'owner': repo.user.username,
2352 'fork_of': repo.fork.repo_name if repo.fork else None,
2351 'fork_of': repo.fork.repo_name if repo.fork else None,
2353 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2352 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2354 'enable_statistics': repo.enable_statistics,
2353 'enable_statistics': repo.enable_statistics,
2355 'enable_locking': repo.enable_locking,
2354 'enable_locking': repo.enable_locking,
2356 'enable_downloads': repo.enable_downloads,
2355 'enable_downloads': repo.enable_downloads,
2357 'last_changeset': repo.changeset_cache,
2356 'last_changeset': repo.changeset_cache,
2358 'locked_by': User.get(_user_id).get_api_data(
2357 'locked_by': User.get(_user_id).get_api_data(
2359 include_secrets=include_secrets) if _user_id else None,
2358 include_secrets=include_secrets) if _user_id else None,
2360 'locked_date': time_to_datetime(_time) if _time else None,
2359 'locked_date': time_to_datetime(_time) if _time else None,
2361 'lock_reason': _reason if _reason else None,
2360 'lock_reason': _reason if _reason else None,
2362 }
2361 }
2363
2362
2364 # TODO: mikhail: should be per-repo settings here
2363 # TODO: mikhail: should be per-repo settings here
2365 rc_config = SettingsModel().get_all_settings()
2364 rc_config = SettingsModel().get_all_settings()
2366 repository_fields = str2bool(
2365 repository_fields = str2bool(
2367 rc_config.get('rhodecode_repository_fields'))
2366 rc_config.get('rhodecode_repository_fields'))
2368 if repository_fields:
2367 if repository_fields:
2369 for f in self.extra_fields:
2368 for f in self.extra_fields:
2370 data[f.field_key_prefixed] = f.field_value
2369 data[f.field_key_prefixed] = f.field_value
2371
2370
2372 return data
2371 return data
2373
2372
2374 @classmethod
2373 @classmethod
2375 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2374 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2376 if not lock_time:
2375 if not lock_time:
2377 lock_time = time.time()
2376 lock_time = time.time()
2378 if not lock_reason:
2377 if not lock_reason:
2379 lock_reason = cls.LOCK_AUTOMATIC
2378 lock_reason = cls.LOCK_AUTOMATIC
2380 repo.locked = [user_id, lock_time, lock_reason]
2379 repo.locked = [user_id, lock_time, lock_reason]
2381 Session().add(repo)
2380 Session().add(repo)
2382 Session().commit()
2381 Session().commit()
2383
2382
2384 @classmethod
2383 @classmethod
2385 def unlock(cls, repo):
2384 def unlock(cls, repo):
2386 repo.locked = None
2385 repo.locked = None
2387 Session().add(repo)
2386 Session().add(repo)
2388 Session().commit()
2387 Session().commit()
2389
2388
2390 @classmethod
2389 @classmethod
2391 def getlock(cls, repo):
2390 def getlock(cls, repo):
2392 return repo.locked
2391 return repo.locked
2393
2392
2394 def get_locking_state(self, action, user_id, only_when_enabled=True):
2393 def get_locking_state(self, action, user_id, only_when_enabled=True):
2395 """
2394 """
2396 Checks locking on this repository, if locking is enabled and lock is
2395 Checks locking on this repository, if locking is enabled and lock is
2397 present returns a tuple of make_lock, locked, locked_by.
2396 present returns a tuple of make_lock, locked, locked_by.
2398 make_lock can have 3 states None (do nothing) True, make lock
2397 make_lock can have 3 states None (do nothing) True, make lock
2399 False release lock, This value is later propagated to hooks, which
2398 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.
2399 do the locking. Think about this as signals passed to hooks what to do.
2401
2400
2402 """
2401 """
2403 # TODO: johbo: This is part of the business logic and should be moved
2402 # TODO: johbo: This is part of the business logic and should be moved
2404 # into the RepositoryModel.
2403 # into the RepositoryModel.
2405
2404
2406 if action not in ('push', 'pull'):
2405 if action not in ('push', 'pull'):
2407 raise ValueError("Invalid action value: %s" % repr(action))
2406 raise ValueError("Invalid action value: %s" % repr(action))
2408
2407
2409 # defines if locked error should be thrown to user
2408 # defines if locked error should be thrown to user
2410 currently_locked = False
2409 currently_locked = False
2411 # defines if new lock should be made, tri-state
2410 # defines if new lock should be made, tri-state
2412 make_lock = None
2411 make_lock = None
2413 repo = self
2412 repo = self
2414 user = User.get(user_id)
2413 user = User.get(user_id)
2415
2414
2416 lock_info = repo.locked
2415 lock_info = repo.locked
2417
2416
2418 if repo and (repo.enable_locking or not only_when_enabled):
2417 if repo and (repo.enable_locking or not only_when_enabled):
2419 if action == 'push':
2418 if action == 'push':
2420 # check if it's already locked !, if it is compare users
2419 # check if it's already locked !, if it is compare users
2421 locked_by_user_id = lock_info[0]
2420 locked_by_user_id = lock_info[0]
2422 if user.user_id == locked_by_user_id:
2421 if user.user_id == locked_by_user_id:
2423 log.debug(
2422 log.debug(
2424 'Got `push` action from user %s, now unlocking', user)
2423 'Got `push` action from user %s, now unlocking', user)
2425 # unlock if we have push from user who locked
2424 # unlock if we have push from user who locked
2426 make_lock = False
2425 make_lock = False
2427 else:
2426 else:
2428 # we're not the same user who locked, ban with
2427 # we're not the same user who locked, ban with
2429 # code defined in settings (default is 423 HTTP Locked) !
2428 # code defined in settings (default is 423 HTTP Locked) !
2430 log.debug('Repo %s is currently locked by %s', repo, user)
2429 log.debug('Repo %s is currently locked by %s', repo, user)
2431 currently_locked = True
2430 currently_locked = True
2432 elif action == 'pull':
2431 elif action == 'pull':
2433 # [0] user [1] date
2432 # [0] user [1] date
2434 if lock_info[0] and lock_info[1]:
2433 if lock_info[0] and lock_info[1]:
2435 log.debug('Repo %s is currently locked by %s', repo, user)
2434 log.debug('Repo %s is currently locked by %s', repo, user)
2436 currently_locked = True
2435 currently_locked = True
2437 else:
2436 else:
2438 log.debug('Setting lock on repo %s by %s', repo, user)
2437 log.debug('Setting lock on repo %s by %s', repo, user)
2439 make_lock = True
2438 make_lock = True
2440
2439
2441 else:
2440 else:
2442 log.debug('Repository %s do not have locking enabled', repo)
2441 log.debug('Repository %s do not have locking enabled', repo)
2443
2442
2444 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2443 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2445 make_lock, currently_locked, lock_info)
2444 make_lock, currently_locked, lock_info)
2446
2445
2447 from rhodecode.lib.auth import HasRepoPermissionAny
2446 from rhodecode.lib.auth import HasRepoPermissionAny
2448 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2447 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2449 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2448 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
2449 # 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 '
2450 log.debug('lock state reset back to FALSE due to lack '
2452 'of at least read permission')
2451 'of at least read permission')
2453 make_lock = False
2452 make_lock = False
2454
2453
2455 return make_lock, currently_locked, lock_info
2454 return make_lock, currently_locked, lock_info
2456
2455
2457 @property
2456 @property
2458 def last_commit_cache_update_diff(self):
2457 def last_commit_cache_update_diff(self):
2459 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2458 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2460
2459
2461 @classmethod
2460 @classmethod
2462 def _load_commit_change(cls, last_commit_cache):
2461 def _load_commit_change(cls, last_commit_cache):
2463 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2462 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2464 empty_date = datetime.datetime.fromtimestamp(0)
2463 empty_date = datetime.datetime.fromtimestamp(0)
2465 date_latest = last_commit_cache.get('date', empty_date)
2464 date_latest = last_commit_cache.get('date', empty_date)
2466 try:
2465 try:
2467 return parse_datetime(date_latest)
2466 return parse_datetime(date_latest)
2468 except Exception:
2467 except Exception:
2469 return empty_date
2468 return empty_date
2470
2469
2471 @property
2470 @property
2472 def last_commit_change(self):
2471 def last_commit_change(self):
2473 return self._load_commit_change(self.changeset_cache)
2472 return self._load_commit_change(self.changeset_cache)
2474
2473
2475 @property
2474 @property
2476 def last_db_change(self):
2475 def last_db_change(self):
2477 return self.updated_on
2476 return self.updated_on
2478
2477
2479 @property
2478 @property
2480 def clone_uri_hidden(self):
2479 def clone_uri_hidden(self):
2481 clone_uri = self.clone_uri
2480 clone_uri = self.clone_uri
2482 if clone_uri:
2481 if clone_uri:
2483 import urlobject
2482 import urlobject
2484 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2483 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2485 if url_obj.password:
2484 if url_obj.password:
2486 clone_uri = url_obj.with_password('*****')
2485 clone_uri = url_obj.with_password('*****')
2487 return clone_uri
2486 return clone_uri
2488
2487
2489 @property
2488 @property
2490 def push_uri_hidden(self):
2489 def push_uri_hidden(self):
2491 push_uri = self.push_uri
2490 push_uri = self.push_uri
2492 if push_uri:
2491 if push_uri:
2493 import urlobject
2492 import urlobject
2494 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2493 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2495 if url_obj.password:
2494 if url_obj.password:
2496 push_uri = url_obj.with_password('*****')
2495 push_uri = url_obj.with_password('*****')
2497 return push_uri
2496 return push_uri
2498
2497
2499 def clone_url(self, **override):
2498 def clone_url(self, **override):
2500 from rhodecode.model.settings import SettingsModel
2499 from rhodecode.model.settings import SettingsModel
2501
2500
2502 uri_tmpl = None
2501 uri_tmpl = None
2503 if 'with_id' in override:
2502 if 'with_id' in override:
2504 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2503 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2505 del override['with_id']
2504 del override['with_id']
2506
2505
2507 if 'uri_tmpl' in override:
2506 if 'uri_tmpl' in override:
2508 uri_tmpl = override['uri_tmpl']
2507 uri_tmpl = override['uri_tmpl']
2509 del override['uri_tmpl']
2508 del override['uri_tmpl']
2510
2509
2511 ssh = False
2510 ssh = False
2512 if 'ssh' in override:
2511 if 'ssh' in override:
2513 ssh = True
2512 ssh = True
2514 del override['ssh']
2513 del override['ssh']
2515
2514
2516 # we didn't override our tmpl from **overrides
2515 # we didn't override our tmpl from **overrides
2517 request = get_current_request()
2516 request = get_current_request()
2518 if not uri_tmpl:
2517 if not uri_tmpl:
2519 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2518 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2520 rc_config = request.call_context.rc_config
2519 rc_config = request.call_context.rc_config
2521 else:
2520 else:
2522 rc_config = SettingsModel().get_all_settings(cache=True)
2521 rc_config = SettingsModel().get_all_settings(cache=True)
2523
2522
2524 if ssh:
2523 if ssh:
2525 uri_tmpl = rc_config.get(
2524 uri_tmpl = rc_config.get(
2526 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2525 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2527
2526
2528 else:
2527 else:
2529 uri_tmpl = rc_config.get(
2528 uri_tmpl = rc_config.get(
2530 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2529 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2531
2530
2532 return get_clone_url(request=request,
2531 return get_clone_url(request=request,
2533 uri_tmpl=uri_tmpl,
2532 uri_tmpl=uri_tmpl,
2534 repo_name=self.repo_name,
2533 repo_name=self.repo_name,
2535 repo_id=self.repo_id,
2534 repo_id=self.repo_id,
2536 repo_type=self.repo_type,
2535 repo_type=self.repo_type,
2537 **override)
2536 **override)
2538
2537
2539 def set_state(self, state):
2538 def set_state(self, state):
2540 self.repo_state = state
2539 self.repo_state = state
2541 Session().add(self)
2540 Session().add(self)
2542 #==========================================================================
2541 #==========================================================================
2543 # SCM PROPERTIES
2542 # SCM PROPERTIES
2544 #==========================================================================
2543 #==========================================================================
2545
2544
2546 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2545 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2547 return get_commit_safe(
2546 return get_commit_safe(
2548 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2547 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2549 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2548 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2550
2549
2551 def get_changeset(self, rev=None, pre_load=None):
2550 def get_changeset(self, rev=None, pre_load=None):
2552 warnings.warn("Use get_commit", DeprecationWarning)
2551 warnings.warn("Use get_commit", DeprecationWarning)
2553 commit_id = None
2552 commit_id = None
2554 commit_idx = None
2553 commit_idx = None
2555 if isinstance(rev, str):
2554 if isinstance(rev, str):
2556 commit_id = rev
2555 commit_id = rev
2557 else:
2556 else:
2558 commit_idx = rev
2557 commit_idx = rev
2559 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2558 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2560 pre_load=pre_load)
2559 pre_load=pre_load)
2561
2560
2562 def get_landing_commit(self):
2561 def get_landing_commit(self):
2563 """
2562 """
2564 Returns landing commit, or if that doesn't exist returns the tip
2563 Returns landing commit, or if that doesn't exist returns the tip
2565 """
2564 """
2566 _rev_type, _rev = self.landing_rev
2565 _rev_type, _rev = self.landing_rev
2567 commit = self.get_commit(_rev)
2566 commit = self.get_commit(_rev)
2568 if isinstance(commit, EmptyCommit):
2567 if isinstance(commit, EmptyCommit):
2569 return self.get_commit()
2568 return self.get_commit()
2570 return commit
2569 return commit
2571
2570
2572 def flush_commit_cache(self):
2571 def flush_commit_cache(self):
2573 self.update_commit_cache(cs_cache={'raw_id':'0'})
2572 self.update_commit_cache(cs_cache={'raw_id':'0'})
2574 self.update_commit_cache()
2573 self.update_commit_cache()
2575
2574
2576 def update_commit_cache(self, cs_cache=None, config=None):
2575 def update_commit_cache(self, cs_cache=None, config=None):
2577 """
2576 """
2578 Update cache of last commit for repository
2577 Update cache of last commit for repository
2579 cache_keys should be::
2578 cache_keys should be::
2580
2579
2581 source_repo_id
2580 source_repo_id
2582 short_id
2581 short_id
2583 raw_id
2582 raw_id
2584 revision
2583 revision
2585 parents
2584 parents
2586 message
2585 message
2587 date
2586 date
2588 author
2587 author
2589 updated_on
2588 updated_on
2590
2589
2591 """
2590 """
2592 from rhodecode.lib.vcs.backends.base import BaseCommit
2591 from rhodecode.lib.vcs.backends.base import BaseCommit
2593 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2592 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2594 empty_date = datetime.datetime.fromtimestamp(0)
2593 empty_date = datetime.datetime.fromtimestamp(0)
2595 repo_commit_count = 0
2594 repo_commit_count = 0
2596
2595
2597 if cs_cache is None:
2596 if cs_cache is None:
2598 # use no-cache version here
2597 # use no-cache version here
2599 try:
2598 try:
2600 scm_repo = self.scm_instance(cache=False, config=config)
2599 scm_repo = self.scm_instance(cache=False, config=config)
2601 except VCSError:
2600 except VCSError:
2602 scm_repo = None
2601 scm_repo = None
2603 empty = scm_repo is None or scm_repo.is_empty()
2602 empty = scm_repo is None or scm_repo.is_empty()
2604
2603
2605 if not empty:
2604 if not empty:
2606 cs_cache = scm_repo.get_commit(
2605 cs_cache = scm_repo.get_commit(
2607 pre_load=["author", "date", "message", "parents", "branch"])
2606 pre_load=["author", "date", "message", "parents", "branch"])
2608 repo_commit_count = scm_repo.count()
2607 repo_commit_count = scm_repo.count()
2609 else:
2608 else:
2610 cs_cache = EmptyCommit()
2609 cs_cache = EmptyCommit()
2611
2610
2612 if isinstance(cs_cache, BaseCommit):
2611 if isinstance(cs_cache, BaseCommit):
2613 cs_cache = cs_cache.__json__()
2612 cs_cache = cs_cache.__json__()
2614
2613
2615 def is_outdated(new_cs_cache):
2614 def is_outdated(new_cs_cache):
2616 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2615 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2617 new_cs_cache['revision'] != self.changeset_cache['revision']):
2616 new_cs_cache['revision'] != self.changeset_cache['revision']):
2618 return True
2617 return True
2619 return False
2618 return False
2620
2619
2621 # check if we have maybe already latest cached revision
2620 # check if we have maybe already latest cached revision
2622 if is_outdated(cs_cache) or not self.changeset_cache:
2621 if is_outdated(cs_cache) or not self.changeset_cache:
2623 _current_datetime = datetime.datetime.utcnow()
2622 _current_datetime = datetime.datetime.utcnow()
2624 last_change = cs_cache.get('date') or _current_datetime
2623 last_change = cs_cache.get('date') or _current_datetime
2625 # we check if last update is newer than the new value
2624 # we check if last update is newer than the new value
2626 # if yes, we use the current timestamp instead. Imagine you get
2625 # 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.
2626 # old commit pushed 1y ago, we'd set last update 1y to ago.
2628 last_change_timestamp = datetime_to_time(last_change)
2627 last_change_timestamp = datetime_to_time(last_change)
2629 current_timestamp = datetime_to_time(last_change)
2628 current_timestamp = datetime_to_time(last_change)
2630 if last_change_timestamp > current_timestamp and not empty:
2629 if last_change_timestamp > current_timestamp and not empty:
2631 cs_cache['date'] = _current_datetime
2630 cs_cache['date'] = _current_datetime
2632
2631
2633 # also store size of repo
2632 # also store size of repo
2634 cs_cache['repo_commit_count'] = repo_commit_count
2633 cs_cache['repo_commit_count'] = repo_commit_count
2635
2634
2636 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2635 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2637 cs_cache['updated_on'] = time.time()
2636 cs_cache['updated_on'] = time.time()
2638 self.changeset_cache = cs_cache
2637 self.changeset_cache = cs_cache
2639 self.updated_on = last_change
2638 self.updated_on = last_change
2640 Session().add(self)
2639 Session().add(self)
2641 Session().commit()
2640 Session().commit()
2642
2641
2643 else:
2642 else:
2644 if empty:
2643 if empty:
2645 cs_cache = EmptyCommit().__json__()
2644 cs_cache = EmptyCommit().__json__()
2646 else:
2645 else:
2647 cs_cache = self.changeset_cache
2646 cs_cache = self.changeset_cache
2648
2647
2649 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2648 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2650
2649
2651 cs_cache['updated_on'] = time.time()
2650 cs_cache['updated_on'] = time.time()
2652 self.changeset_cache = cs_cache
2651 self.changeset_cache = cs_cache
2653 self.updated_on = _date_latest
2652 self.updated_on = _date_latest
2654 Session().add(self)
2653 Session().add(self)
2655 Session().commit()
2654 Session().commit()
2656
2655
2657 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2656 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2658 self.repo_name, cs_cache, _date_latest)
2657 self.repo_name, cs_cache, _date_latest)
2659
2658
2660 @property
2659 @property
2661 def tip(self):
2660 def tip(self):
2662 return self.get_commit('tip')
2661 return self.get_commit('tip')
2663
2662
2664 @property
2663 @property
2665 def author(self):
2664 def author(self):
2666 return self.tip.author
2665 return self.tip.author
2667
2666
2668 @property
2667 @property
2669 def last_change(self):
2668 def last_change(self):
2670 return self.scm_instance().last_change
2669 return self.scm_instance().last_change
2671
2670
2672 def get_comments(self, revisions=None):
2671 def get_comments(self, revisions=None):
2673 """
2672 """
2674 Returns comments for this repository grouped by revisions
2673 Returns comments for this repository grouped by revisions
2675
2674
2676 :param revisions: filter query by revisions only
2675 :param revisions: filter query by revisions only
2677 """
2676 """
2678 cmts = ChangesetComment.query()\
2677 cmts = ChangesetComment.query()\
2679 .filter(ChangesetComment.repo == self)
2678 .filter(ChangesetComment.repo == self)
2680 if revisions:
2679 if revisions:
2681 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2680 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2682 grouped = collections.defaultdict(list)
2681 grouped = collections.defaultdict(list)
2683 for cmt in cmts.all():
2682 for cmt in cmts.all():
2684 grouped[cmt.revision].append(cmt)
2683 grouped[cmt.revision].append(cmt)
2685 return grouped
2684 return grouped
2686
2685
2687 def statuses(self, revisions=None):
2686 def statuses(self, revisions=None):
2688 """
2687 """
2689 Returns statuses for this repository
2688 Returns statuses for this repository
2690
2689
2691 :param revisions: list of revisions to get statuses for
2690 :param revisions: list of revisions to get statuses for
2692 """
2691 """
2693 statuses = ChangesetStatus.query()\
2692 statuses = ChangesetStatus.query()\
2694 .filter(ChangesetStatus.repo == self)\
2693 .filter(ChangesetStatus.repo == self)\
2695 .filter(ChangesetStatus.version == 0)
2694 .filter(ChangesetStatus.version == 0)
2696
2695
2697 if revisions:
2696 if revisions:
2698 # Try doing the filtering in chunks to avoid hitting limits
2697 # Try doing the filtering in chunks to avoid hitting limits
2699 size = 500
2698 size = 500
2700 status_results = []
2699 status_results = []
2701 for chunk in range(0, len(revisions), size):
2700 for chunk in range(0, len(revisions), size):
2702 status_results += statuses.filter(
2701 status_results += statuses.filter(
2703 ChangesetStatus.revision.in_(
2702 ChangesetStatus.revision.in_(
2704 revisions[chunk: chunk+size])
2703 revisions[chunk: chunk+size])
2705 ).all()
2704 ).all()
2706 else:
2705 else:
2707 status_results = statuses.all()
2706 status_results = statuses.all()
2708
2707
2709 grouped = {}
2708 grouped = {}
2710
2709
2711 # maybe we have open new pullrequest without a status?
2710 # maybe we have open new pullrequest without a status?
2712 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2711 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2713 status_lbl = ChangesetStatus.get_status_lbl(stat)
2712 status_lbl = ChangesetStatus.get_status_lbl(stat)
2714 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2713 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2715 for rev in pr.revisions:
2714 for rev in pr.revisions:
2716 pr_id = pr.pull_request_id
2715 pr_id = pr.pull_request_id
2717 pr_repo = pr.target_repo.repo_name
2716 pr_repo = pr.target_repo.repo_name
2718 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2717 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2719
2718
2720 for stat in status_results:
2719 for stat in status_results:
2721 pr_id = pr_repo = None
2720 pr_id = pr_repo = None
2722 if stat.pull_request:
2721 if stat.pull_request:
2723 pr_id = stat.pull_request.pull_request_id
2722 pr_id = stat.pull_request.pull_request_id
2724 pr_repo = stat.pull_request.target_repo.repo_name
2723 pr_repo = stat.pull_request.target_repo.repo_name
2725 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2724 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2726 pr_id, pr_repo]
2725 pr_id, pr_repo]
2727 return grouped
2726 return grouped
2728
2727
2729 # ==========================================================================
2728 # ==========================================================================
2730 # SCM CACHE INSTANCE
2729 # SCM CACHE INSTANCE
2731 # ==========================================================================
2730 # ==========================================================================
2732
2731
2733 def scm_instance(self, **kwargs):
2732 def scm_instance(self, **kwargs):
2734 import rhodecode
2733 import rhodecode
2735
2734
2736 # Passing a config will not hit the cache currently only used
2735 # Passing a config will not hit the cache currently only used
2737 # for repo2dbmapper
2736 # for repo2dbmapper
2738 config = kwargs.pop('config', None)
2737 config = kwargs.pop('config', None)
2739 cache = kwargs.pop('cache', None)
2738 cache = kwargs.pop('cache', None)
2740 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2739 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2741 if vcs_full_cache is not None:
2740 if vcs_full_cache is not None:
2742 # allows override global config
2741 # allows override global config
2743 full_cache = vcs_full_cache
2742 full_cache = vcs_full_cache
2744 else:
2743 else:
2745 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2744 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2746 # if cache is NOT defined use default global, else we have a full
2745 # if cache is NOT defined use default global, else we have a full
2747 # control over cache behaviour
2746 # control over cache behaviour
2748 if cache is None and full_cache and not config:
2747 if cache is None and full_cache and not config:
2749 log.debug('Initializing pure cached instance for %s', self.repo_path)
2748 log.debug('Initializing pure cached instance for %s', self.repo_path)
2750 return self._get_instance_cached()
2749 return self._get_instance_cached()
2751
2750
2752 # cache here is sent to the "vcs server"
2751 # cache here is sent to the "vcs server"
2753 return self._get_instance(cache=bool(cache), config=config)
2752 return self._get_instance(cache=bool(cache), config=config)
2754
2753
2755 def _get_instance_cached(self):
2754 def _get_instance_cached(self):
2756 from rhodecode.lib import rc_cache
2755 from rhodecode.lib import rc_cache
2757
2756
2758 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2757 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2759 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2758 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2760
2759
2761 # we must use thread scoped cache here,
2760 # we must use thread scoped cache here,
2762 # because each thread of gevent needs it's own not shared connection and cache
2761 # 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.
2762 # 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)
2763 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)
2764 inv_context_manager = rc_cache.InvalidationContext(key=repo_namespace_key, thread_scoped=True)
2766
2765
2767 # our wrapped caching function that takes state_uid to save the previous state in
2766 # our wrapped caching function that takes state_uid to save the previous state in
2768 def cache_generator(_state_uid):
2767 def cache_generator(_state_uid):
2769
2768
2770 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2769 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2771 def get_instance_cached(_repo_id, _process_context_id):
2770 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
2771 # 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)
2772 return _state_uid, self._get_instance(repo_state_uid=_state_uid)
2774
2773
2775 return get_instance_cached
2774 return get_instance_cached
2776
2775
2777 with inv_context_manager as invalidation_context:
2776 with inv_context_manager as invalidation_context:
2778 cache_state_uid = invalidation_context.state_uid
2777 cache_state_uid = invalidation_context.state_uid
2779 cache_func = cache_generator(cache_state_uid)
2778 cache_func = cache_generator(cache_state_uid)
2780
2779
2781 args = self.repo_id, inv_context_manager.proc_key
2780 args = self.repo_id, inv_context_manager.proc_key
2782
2781
2783 previous_state_uid, instance = cache_func(*args)
2782 previous_state_uid, instance = cache_func(*args)
2784
2783
2785 # now compare keys, the "cache" state vs expected state.
2784 # now compare keys, the "cache" state vs expected state.
2786 if previous_state_uid != cache_state_uid:
2785 if previous_state_uid != cache_state_uid:
2787 log.warning('Cached state uid %s is different than current state uid %s',
2786 log.warning('Cached state uid %s is different than current state uid %s',
2788 previous_state_uid, cache_state_uid)
2787 previous_state_uid, cache_state_uid)
2789 _, instance = cache_func.refresh(*args)
2788 _, instance = cache_func.refresh(*args)
2790
2789
2791 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2790 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2792 return instance
2791 return instance
2793
2792
2794 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2793 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',
2794 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2796 self.repo_type, self.repo_path, cache)
2795 self.repo_type, self.repo_path, cache)
2797 config = config or self._config
2796 config = config or self._config
2798 custom_wire = {
2797 custom_wire = {
2799 'cache': cache, # controls the vcs.remote cache
2798 'cache': cache, # controls the vcs.remote cache
2800 'repo_state_uid': repo_state_uid
2799 'repo_state_uid': repo_state_uid
2801 }
2800 }
2802
2801
2803 repo = get_vcs_instance(
2802 repo = get_vcs_instance(
2804 repo_path=safe_str(self.repo_full_path),
2803 repo_path=safe_str(self.repo_full_path),
2805 config=config,
2804 config=config,
2806 with_wire=custom_wire,
2805 with_wire=custom_wire,
2807 create=False,
2806 create=False,
2808 _vcs_alias=self.repo_type)
2807 _vcs_alias=self.repo_type)
2809 if repo is not None:
2808 if repo is not None:
2810 repo.count() # cache rebuild
2809 repo.count() # cache rebuild
2811
2810
2812 return repo
2811 return repo
2813
2812
2814 def get_shadow_repository_path(self, workspace_id):
2813 def get_shadow_repository_path(self, workspace_id):
2815 from rhodecode.lib.vcs.backends.base import BaseRepository
2814 from rhodecode.lib.vcs.backends.base import BaseRepository
2816 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2815 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2817 self.repo_full_path, self.repo_id, workspace_id)
2816 self.repo_full_path, self.repo_id, workspace_id)
2818 return shadow_repo_path
2817 return shadow_repo_path
2819
2818
2820 def __json__(self):
2819 def __json__(self):
2821 return {'landing_rev': self.landing_rev}
2820 return {'landing_rev': self.landing_rev}
2822
2821
2823 def get_dict(self):
2822 def get_dict(self):
2824
2823
2825 # Since we transformed `repo_name` to a hybrid property, we need to
2824 # Since we transformed `repo_name` to a hybrid property, we need to
2826 # keep compatibility with the code which uses `repo_name` field.
2825 # keep compatibility with the code which uses `repo_name` field.
2827
2826
2828 result = super(Repository, self).get_dict()
2827 result = super(Repository, self).get_dict()
2829 result['repo_name'] = result.pop('_repo_name', None)
2828 result['repo_name'] = result.pop('_repo_name', None)
2830 result.pop('_changeset_cache', '')
2829 result.pop('_changeset_cache', '')
2831 return result
2830 return result
2832
2831
2833
2832
2834 class RepoGroup(Base, BaseModel):
2833 class RepoGroup(Base, BaseModel):
2835 __tablename__ = 'groups'
2834 __tablename__ = 'groups'
2836 __table_args__ = (
2835 __table_args__ = (
2837 UniqueConstraint('group_name', 'group_parent_id'),
2836 UniqueConstraint('group_name', 'group_parent_id'),
2838 base_table_args,
2837 base_table_args,
2839 )
2838 )
2840
2839
2841 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2840 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2842
2841
2843 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2842 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)
2843 _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)
2844 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)
2845 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)
2846 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)
2847 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)
2848 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)
2849 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)
2850 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)
2851 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2853 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2852 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2854
2853
2855 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2854 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')
2855 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2857 parent_group = relationship('RepoGroup', remote_side=group_id)
2856 parent_group = relationship('RepoGroup', remote_side=group_id)
2858 user = relationship('User', back_populates='repository_groups')
2857 user = relationship('User', back_populates='repository_groups')
2859 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2858 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2860
2859
2861 # no cascade, set NULL
2860 # no cascade, set NULL
2862 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2861 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2863
2862
2864 def __init__(self, group_name='', parent_group=None):
2863 def __init__(self, group_name='', parent_group=None):
2865 self.group_name = group_name
2864 self.group_name = group_name
2866 self.parent_group = parent_group
2865 self.parent_group = parent_group
2867
2866
2868 def __repr__(self):
2867 def __repr__(self):
2869 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2868 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2870
2869
2871 @hybrid_property
2870 @hybrid_property
2872 def group_name(self):
2871 def group_name(self):
2873 return self._group_name
2872 return self._group_name
2874
2873
2875 @group_name.setter
2874 @group_name.setter
2876 def group_name(self, value):
2875 def group_name(self, value):
2877 self._group_name = value
2876 self._group_name = value
2878 self.group_name_hash = self.hash_repo_group_name(value)
2877 self.group_name_hash = self.hash_repo_group_name(value)
2879
2878
2880 @classmethod
2879 @classmethod
2881 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2880 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2882 from rhodecode.lib.vcs.backends.base import EmptyCommit
2881 from rhodecode.lib.vcs.backends.base import EmptyCommit
2883 dummy = EmptyCommit().__json__()
2882 dummy = EmptyCommit().__json__()
2884 if not changeset_cache_raw:
2883 if not changeset_cache_raw:
2885 dummy['source_repo_id'] = repo_id
2884 dummy['source_repo_id'] = repo_id
2886 return json.loads(json.dumps(dummy))
2885 return json.loads(json.dumps(dummy))
2887
2886
2888 try:
2887 try:
2889 return json.loads(changeset_cache_raw)
2888 return json.loads(changeset_cache_raw)
2890 except TypeError:
2889 except TypeError:
2891 return dummy
2890 return dummy
2892 except Exception:
2891 except Exception:
2893 log.error(traceback.format_exc())
2892 log.error(traceback.format_exc())
2894 return dummy
2893 return dummy
2895
2894
2896 @hybrid_property
2895 @hybrid_property
2897 def changeset_cache(self):
2896 def changeset_cache(self):
2898 return self._load_changeset_cache('', self._changeset_cache)
2897 return self._load_changeset_cache('', self._changeset_cache)
2899
2898
2900 @changeset_cache.setter
2899 @changeset_cache.setter
2901 def changeset_cache(self, val):
2900 def changeset_cache(self, val):
2902 try:
2901 try:
2903 self._changeset_cache = json.dumps(val)
2902 self._changeset_cache = json.dumps(val)
2904 except Exception:
2903 except Exception:
2905 log.error(traceback.format_exc())
2904 log.error(traceback.format_exc())
2906
2905
2907 @validates('group_parent_id')
2906 @validates('group_parent_id')
2908 def validate_group_parent_id(self, key, val):
2907 def validate_group_parent_id(self, key, val):
2909 """
2908 """
2910 Check cycle references for a parent group to self
2909 Check cycle references for a parent group to self
2911 """
2910 """
2912 if self.group_id and val:
2911 if self.group_id and val:
2913 assert val != self.group_id
2912 assert val != self.group_id
2914
2913
2915 return val
2914 return val
2916
2915
2917 @hybrid_property
2916 @hybrid_property
2918 def description_safe(self):
2917 def description_safe(self):
2919 from rhodecode.lib import helpers as h
2918 from rhodecode.lib import helpers as h
2920 return h.escape(self.group_description)
2919 return h.escape(self.group_description)
2921
2920
2922 @classmethod
2921 @classmethod
2923 def hash_repo_group_name(cls, repo_group_name):
2922 def hash_repo_group_name(cls, repo_group_name):
2924 val = remove_formatting(repo_group_name)
2923 val = remove_formatting(repo_group_name)
2925 val = safe_str(val).lower()
2924 val = safe_str(val).lower()
2926 chars = []
2925 chars = []
2927 for c in val:
2926 for c in val:
2928 if c not in string.ascii_letters:
2927 if c not in string.ascii_letters:
2929 c = str(ord(c))
2928 c = str(ord(c))
2930 chars.append(c)
2929 chars.append(c)
2931
2930
2932 return ''.join(chars)
2931 return ''.join(chars)
2933
2932
2934 @classmethod
2933 @classmethod
2935 def _generate_choice(cls, repo_group):
2934 def _generate_choice(cls, repo_group):
2936 from webhelpers2.html import literal as _literal
2935 from webhelpers2.html import literal as _literal
2937
2936
2938 def _name(k):
2937 def _name(k):
2939 return _literal(cls.CHOICES_SEPARATOR.join(k))
2938 return _literal(cls.CHOICES_SEPARATOR.join(k))
2940
2939
2941 return repo_group.group_id, _name(repo_group.full_path_splitted)
2940 return repo_group.group_id, _name(repo_group.full_path_splitted)
2942
2941
2943 @classmethod
2942 @classmethod
2944 def groups_choices(cls, groups=None, show_empty_group=True):
2943 def groups_choices(cls, groups=None, show_empty_group=True):
2945 if not groups:
2944 if not groups:
2946 groups = cls.query().all()
2945 groups = cls.query().all()
2947
2946
2948 repo_groups = []
2947 repo_groups = []
2949 if show_empty_group:
2948 if show_empty_group:
2950 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2949 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2951
2950
2952 repo_groups.extend([cls._generate_choice(x) for x in groups])
2951 repo_groups.extend([cls._generate_choice(x) for x in groups])
2953
2952
2954 repo_groups = sorted(
2953 repo_groups = sorted(
2955 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2954 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2956 return repo_groups
2955 return repo_groups
2957
2956
2958 @classmethod
2957 @classmethod
2959 def url_sep(cls):
2958 def url_sep(cls):
2960 return URL_SEP
2959 return URL_SEP
2961
2960
2962 @classmethod
2961 @classmethod
2963 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2962 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2964 if case_insensitive:
2963 if case_insensitive:
2965 gr = cls.query().filter(func.lower(cls.group_name)
2964 gr = cls.query().filter(func.lower(cls.group_name)
2966 == func.lower(group_name))
2965 == func.lower(group_name))
2967 else:
2966 else:
2968 gr = cls.query().filter(cls.group_name == group_name)
2967 gr = cls.query().filter(cls.group_name == group_name)
2969 if cache:
2968 if cache:
2970 name_key = _hash_key(group_name)
2969 name_key = _hash_key(group_name)
2971 gr = gr.options(
2970 gr = gr.options(
2972 FromCache("sql_cache_short", f"get_group_{name_key}"))
2971 FromCache("sql_cache_short", f"get_group_{name_key}"))
2973 return gr.scalar()
2972 return gr.scalar()
2974
2973
2975 @classmethod
2974 @classmethod
2976 def get_user_personal_repo_group(cls, user_id):
2975 def get_user_personal_repo_group(cls, user_id):
2977 user = User.get(user_id)
2976 user = User.get(user_id)
2978 if user.username == User.DEFAULT_USER:
2977 if user.username == User.DEFAULT_USER:
2979 return None
2978 return None
2980
2979
2981 return cls.query()\
2980 return cls.query()\
2982 .filter(cls.personal == true()) \
2981 .filter(cls.personal == true()) \
2983 .filter(cls.user == user) \
2982 .filter(cls.user == user) \
2984 .order_by(cls.group_id.asc()) \
2983 .order_by(cls.group_id.asc()) \
2985 .first()
2984 .first()
2986
2985
2987 @classmethod
2986 @classmethod
2988 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2987 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2989 case_insensitive=True):
2988 case_insensitive=True):
2990 q = RepoGroup.query()
2989 q = RepoGroup.query()
2991
2990
2992 if not isinstance(user_id, Optional):
2991 if not isinstance(user_id, Optional):
2993 q = q.filter(RepoGroup.user_id == user_id)
2992 q = q.filter(RepoGroup.user_id == user_id)
2994
2993
2995 if not isinstance(group_id, Optional):
2994 if not isinstance(group_id, Optional):
2996 q = q.filter(RepoGroup.group_parent_id == group_id)
2995 q = q.filter(RepoGroup.group_parent_id == group_id)
2997
2996
2998 if case_insensitive:
2997 if case_insensitive:
2999 q = q.order_by(func.lower(RepoGroup.group_name))
2998 q = q.order_by(func.lower(RepoGroup.group_name))
3000 else:
2999 else:
3001 q = q.order_by(RepoGroup.group_name)
3000 q = q.order_by(RepoGroup.group_name)
3002 return q.all()
3001 return q.all()
3003
3002
3004 @property
3003 @property
3005 def parents(self, parents_recursion_limit=10):
3004 def parents(self, parents_recursion_limit=10):
3006 groups = []
3005 groups = []
3007 if self.parent_group is None:
3006 if self.parent_group is None:
3008 return groups
3007 return groups
3009 cur_gr = self.parent_group
3008 cur_gr = self.parent_group
3010 groups.insert(0, cur_gr)
3009 groups.insert(0, cur_gr)
3011 cnt = 0
3010 cnt = 0
3012 while 1:
3011 while 1:
3013 cnt += 1
3012 cnt += 1
3014 gr = getattr(cur_gr, 'parent_group', None)
3013 gr = getattr(cur_gr, 'parent_group', None)
3015 cur_gr = cur_gr.parent_group
3014 cur_gr = cur_gr.parent_group
3016 if gr is None:
3015 if gr is None:
3017 break
3016 break
3018 if cnt == parents_recursion_limit:
3017 if cnt == parents_recursion_limit:
3019 # this will prevent accidental infinit loops
3018 # this will prevent accidental infinit loops
3020 log.error('more than %s parents found for group %s, stopping '
3019 log.error('more than %s parents found for group %s, stopping '
3021 'recursive parent fetching', parents_recursion_limit, self)
3020 'recursive parent fetching', parents_recursion_limit, self)
3022 break
3021 break
3023
3022
3024 groups.insert(0, gr)
3023 groups.insert(0, gr)
3025 return groups
3024 return groups
3026
3025
3027 @property
3026 @property
3028 def last_commit_cache_update_diff(self):
3027 def last_commit_cache_update_diff(self):
3029 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3028 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
3030
3029
3031 @classmethod
3030 @classmethod
3032 def _load_commit_change(cls, last_commit_cache):
3031 def _load_commit_change(cls, last_commit_cache):
3033 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3032 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3034 empty_date = datetime.datetime.fromtimestamp(0)
3033 empty_date = datetime.datetime.fromtimestamp(0)
3035 date_latest = last_commit_cache.get('date', empty_date)
3034 date_latest = last_commit_cache.get('date', empty_date)
3036 try:
3035 try:
3037 return parse_datetime(date_latest)
3036 return parse_datetime(date_latest)
3038 except Exception:
3037 except Exception:
3039 return empty_date
3038 return empty_date
3040
3039
3041 @property
3040 @property
3042 def last_commit_change(self):
3041 def last_commit_change(self):
3043 return self._load_commit_change(self.changeset_cache)
3042 return self._load_commit_change(self.changeset_cache)
3044
3043
3045 @property
3044 @property
3046 def last_db_change(self):
3045 def last_db_change(self):
3047 return self.updated_on
3046 return self.updated_on
3048
3047
3049 @property
3048 @property
3050 def children(self):
3049 def children(self):
3051 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3050 return RepoGroup.query().filter(RepoGroup.parent_group == self)
3052
3051
3053 @property
3052 @property
3054 def name(self):
3053 def name(self):
3055 return self.group_name.split(RepoGroup.url_sep())[-1]
3054 return self.group_name.split(RepoGroup.url_sep())[-1]
3056
3055
3057 @property
3056 @property
3058 def full_path(self):
3057 def full_path(self):
3059 return self.group_name
3058 return self.group_name
3060
3059
3061 @property
3060 @property
3062 def full_path_splitted(self):
3061 def full_path_splitted(self):
3063 return self.group_name.split(RepoGroup.url_sep())
3062 return self.group_name.split(RepoGroup.url_sep())
3064
3063
3065 @property
3064 @property
3066 def repositories(self):
3065 def repositories(self):
3067 return Repository.query()\
3066 return Repository.query()\
3068 .filter(Repository.group == self)\
3067 .filter(Repository.group == self)\
3069 .order_by(Repository.repo_name)
3068 .order_by(Repository.repo_name)
3070
3069
3071 @property
3070 @property
3072 def repositories_recursive_count(self):
3071 def repositories_recursive_count(self):
3073 cnt = self.repositories.count()
3072 cnt = self.repositories.count()
3074
3073
3075 def children_count(group):
3074 def children_count(group):
3076 cnt = 0
3075 cnt = 0
3077 for child in group.children:
3076 for child in group.children:
3078 cnt += child.repositories.count()
3077 cnt += child.repositories.count()
3079 cnt += children_count(child)
3078 cnt += children_count(child)
3080 return cnt
3079 return cnt
3081
3080
3082 return cnt + children_count(self)
3081 return cnt + children_count(self)
3083
3082
3084 def _recursive_objects(self, include_repos=True, include_groups=True):
3083 def _recursive_objects(self, include_repos=True, include_groups=True):
3085 all_ = []
3084 all_ = []
3086
3085
3087 def _get_members(root_gr):
3086 def _get_members(root_gr):
3088 if include_repos:
3087 if include_repos:
3089 for r in root_gr.repositories:
3088 for r in root_gr.repositories:
3090 all_.append(r)
3089 all_.append(r)
3091 childs = root_gr.children.all()
3090 childs = root_gr.children.all()
3092 if childs:
3091 if childs:
3093 for gr in childs:
3092 for gr in childs:
3094 if include_groups:
3093 if include_groups:
3095 all_.append(gr)
3094 all_.append(gr)
3096 _get_members(gr)
3095 _get_members(gr)
3097
3096
3098 root_group = []
3097 root_group = []
3099 if include_groups:
3098 if include_groups:
3100 root_group = [self]
3099 root_group = [self]
3101
3100
3102 _get_members(self)
3101 _get_members(self)
3103 return root_group + all_
3102 return root_group + all_
3104
3103
3105 def recursive_groups_and_repos(self):
3104 def recursive_groups_and_repos(self):
3106 """
3105 """
3107 Recursive return all groups, with repositories in those groups
3106 Recursive return all groups, with repositories in those groups
3108 """
3107 """
3109 return self._recursive_objects()
3108 return self._recursive_objects()
3110
3109
3111 def recursive_groups(self):
3110 def recursive_groups(self):
3112 """
3111 """
3113 Returns all children groups for this group including children of children
3112 Returns all children groups for this group including children of children
3114 """
3113 """
3115 return self._recursive_objects(include_repos=False)
3114 return self._recursive_objects(include_repos=False)
3116
3115
3117 def recursive_repos(self):
3116 def recursive_repos(self):
3118 """
3117 """
3119 Returns all children repositories for this group
3118 Returns all children repositories for this group
3120 """
3119 """
3121 return self._recursive_objects(include_groups=False)
3120 return self._recursive_objects(include_groups=False)
3122
3121
3123 def get_new_name(self, group_name):
3122 def get_new_name(self, group_name):
3124 """
3123 """
3125 returns new full group name based on parent and new name
3124 returns new full group name based on parent and new name
3126
3125
3127 :param group_name:
3126 :param group_name:
3128 """
3127 """
3129 path_prefix = (self.parent_group.full_path_splitted if
3128 path_prefix = (self.parent_group.full_path_splitted if
3130 self.parent_group else [])
3129 self.parent_group else [])
3131 return RepoGroup.url_sep().join(path_prefix + [group_name])
3130 return RepoGroup.url_sep().join(path_prefix + [group_name])
3132
3131
3133 def update_commit_cache(self, config=None):
3132 def update_commit_cache(self, config=None):
3134 """
3133 """
3135 Update cache of last commit for newest repository inside this repository group.
3134 Update cache of last commit for newest repository inside this repository group.
3136 cache_keys should be::
3135 cache_keys should be::
3137
3136
3138 source_repo_id
3137 source_repo_id
3139 short_id
3138 short_id
3140 raw_id
3139 raw_id
3141 revision
3140 revision
3142 parents
3141 parents
3143 message
3142 message
3144 date
3143 date
3145 author
3144 author
3146
3145
3147 """
3146 """
3148 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3147 from rhodecode.lib.vcs.utils.helpers import parse_datetime
3149 empty_date = datetime.datetime.fromtimestamp(0)
3148 empty_date = datetime.datetime.fromtimestamp(0)
3150
3149
3151 def repo_groups_and_repos(root_gr):
3150 def repo_groups_and_repos(root_gr):
3152 for _repo in root_gr.repositories:
3151 for _repo in root_gr.repositories:
3153 yield _repo
3152 yield _repo
3154 for child_group in root_gr.children.all():
3153 for child_group in root_gr.children.all():
3155 yield child_group
3154 yield child_group
3156
3155
3157 latest_repo_cs_cache = {}
3156 latest_repo_cs_cache = {}
3158 for obj in repo_groups_and_repos(self):
3157 for obj in repo_groups_and_repos(self):
3159 repo_cs_cache = obj.changeset_cache
3158 repo_cs_cache = obj.changeset_cache
3160 date_latest = latest_repo_cs_cache.get('date', empty_date)
3159 date_latest = latest_repo_cs_cache.get('date', empty_date)
3161 date_current = repo_cs_cache.get('date', empty_date)
3160 date_current = repo_cs_cache.get('date', empty_date)
3162 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3161 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3163 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3162 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3164 latest_repo_cs_cache = repo_cs_cache
3163 latest_repo_cs_cache = repo_cs_cache
3165 if hasattr(obj, 'repo_id'):
3164 if hasattr(obj, 'repo_id'):
3166 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3165 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3167 else:
3166 else:
3168 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3167 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3169
3168
3170 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3169 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3171
3170
3172 latest_repo_cs_cache['updated_on'] = time.time()
3171 latest_repo_cs_cache['updated_on'] = time.time()
3173 self.changeset_cache = latest_repo_cs_cache
3172 self.changeset_cache = latest_repo_cs_cache
3174 self.updated_on = _date_latest
3173 self.updated_on = _date_latest
3175 Session().add(self)
3174 Session().add(self)
3176 Session().commit()
3175 Session().commit()
3177
3176
3178 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3177 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)
3178 self.group_name, latest_repo_cs_cache, _date_latest)
3180
3179
3181 def permissions(self, with_admins=True, with_owner=True,
3180 def permissions(self, with_admins=True, with_owner=True,
3182 expand_from_user_groups=False):
3181 expand_from_user_groups=False):
3183 """
3182 """
3184 Permissions for repository groups
3183 Permissions for repository groups
3185 """
3184 """
3186 _admin_perm = 'group.admin'
3185 _admin_perm = 'group.admin'
3187
3186
3188 owner_row = []
3187 owner_row = []
3189 if with_owner:
3188 if with_owner:
3190 usr = AttributeDict(self.user.get_dict())
3189 usr = AttributeDict(self.user.get_dict())
3191 usr.owner_row = True
3190 usr.owner_row = True
3192 usr.permission = _admin_perm
3191 usr.permission = _admin_perm
3193 owner_row.append(usr)
3192 owner_row.append(usr)
3194
3193
3195 super_admin_ids = []
3194 super_admin_ids = []
3196 super_admin_rows = []
3195 super_admin_rows = []
3197 if with_admins:
3196 if with_admins:
3198 for usr in User.get_all_super_admins():
3197 for usr in User.get_all_super_admins():
3199 super_admin_ids.append(usr.user_id)
3198 super_admin_ids.append(usr.user_id)
3200 # if this admin is also owner, don't double the record
3199 # if this admin is also owner, don't double the record
3201 if usr.user_id == owner_row[0].user_id:
3200 if usr.user_id == owner_row[0].user_id:
3202 owner_row[0].admin_row = True
3201 owner_row[0].admin_row = True
3203 else:
3202 else:
3204 usr = AttributeDict(usr.get_dict())
3203 usr = AttributeDict(usr.get_dict())
3205 usr.admin_row = True
3204 usr.admin_row = True
3206 usr.permission = _admin_perm
3205 usr.permission = _admin_perm
3207 super_admin_rows.append(usr)
3206 super_admin_rows.append(usr)
3208
3207
3209 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3208 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3210 q = q.options(joinedload(UserRepoGroupToPerm.group),
3209 q = q.options(joinedload(UserRepoGroupToPerm.group),
3211 joinedload(UserRepoGroupToPerm.user),
3210 joinedload(UserRepoGroupToPerm.user),
3212 joinedload(UserRepoGroupToPerm.permission),)
3211 joinedload(UserRepoGroupToPerm.permission),)
3213
3212
3214 # get owners and admins and permissions. We do a trick of re-writing
3213 # get owners and admins and permissions. We do a trick of re-writing
3215 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3214 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3216 # has a global reference and changing one object propagates to all
3215 # 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
3216 # others. This means if admin is also an owner admin_row that change
3218 # would propagate to both objects
3217 # would propagate to both objects
3219 perm_rows = []
3218 perm_rows = []
3220 for _usr in q.all():
3219 for _usr in q.all():
3221 usr = AttributeDict(_usr.user.get_dict())
3220 usr = AttributeDict(_usr.user.get_dict())
3222 # if this user is also owner/admin, mark as duplicate record
3221 # 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:
3222 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3224 usr.duplicate_perm = True
3223 usr.duplicate_perm = True
3225 usr.permission = _usr.permission.permission_name
3224 usr.permission = _usr.permission.permission_name
3226 perm_rows.append(usr)
3225 perm_rows.append(usr)
3227
3226
3228 # filter the perm rows by 'default' first and then sort them by
3227 # filter the perm rows by 'default' first and then sort them by
3229 # admin,write,read,none permissions sorted again alphabetically in
3228 # admin,write,read,none permissions sorted again alphabetically in
3230 # each group
3229 # each group
3231 perm_rows = sorted(perm_rows, key=display_user_sort)
3230 perm_rows = sorted(perm_rows, key=display_user_sort)
3232
3231
3233 user_groups_rows = []
3232 user_groups_rows = []
3234 if expand_from_user_groups:
3233 if expand_from_user_groups:
3235 for ug in self.permission_user_groups(with_members=True):
3234 for ug in self.permission_user_groups(with_members=True):
3236 for user_data in ug.members:
3235 for user_data in ug.members:
3237 user_groups_rows.append(user_data)
3236 user_groups_rows.append(user_data)
3238
3237
3239 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3238 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3240
3239
3241 def permission_user_groups(self, with_members=False):
3240 def permission_user_groups(self, with_members=False):
3242 q = UserGroupRepoGroupToPerm.query()\
3241 q = UserGroupRepoGroupToPerm.query()\
3243 .filter(UserGroupRepoGroupToPerm.group == self)
3242 .filter(UserGroupRepoGroupToPerm.group == self)
3244 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3243 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3245 joinedload(UserGroupRepoGroupToPerm.users_group),
3244 joinedload(UserGroupRepoGroupToPerm.users_group),
3246 joinedload(UserGroupRepoGroupToPerm.permission),)
3245 joinedload(UserGroupRepoGroupToPerm.permission),)
3247
3246
3248 perm_rows = []
3247 perm_rows = []
3249 for _user_group in q.all():
3248 for _user_group in q.all():
3250 entry = AttributeDict(_user_group.users_group.get_dict())
3249 entry = AttributeDict(_user_group.users_group.get_dict())
3251 entry.permission = _user_group.permission.permission_name
3250 entry.permission = _user_group.permission.permission_name
3252 if with_members:
3251 if with_members:
3253 entry.members = [x.user.get_dict()
3252 entry.members = [x.user.get_dict()
3254 for x in _user_group.users_group.members]
3253 for x in _user_group.users_group.members]
3255 perm_rows.append(entry)
3254 perm_rows.append(entry)
3256
3255
3257 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3256 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3258 return perm_rows
3257 return perm_rows
3259
3258
3260 def get_api_data(self):
3259 def get_api_data(self):
3261 """
3260 """
3262 Common function for generating api data
3261 Common function for generating api data
3263
3262
3264 """
3263 """
3265 group = self
3264 group = self
3266 data = {
3265 data = {
3267 'group_id': group.group_id,
3266 'group_id': group.group_id,
3268 'group_name': group.group_name,
3267 'group_name': group.group_name,
3269 'group_description': group.description_safe,
3268 'group_description': group.description_safe,
3270 'parent_group': group.parent_group.group_name if group.parent_group else None,
3269 'parent_group': group.parent_group.group_name if group.parent_group else None,
3271 'repositories': [x.repo_name for x in group.repositories],
3270 'repositories': [x.repo_name for x in group.repositories],
3272 'owner': group.user.username,
3271 'owner': group.user.username,
3273 }
3272 }
3274 return data
3273 return data
3275
3274
3276 def get_dict(self):
3275 def get_dict(self):
3277 # Since we transformed `group_name` to a hybrid property, we need to
3276 # Since we transformed `group_name` to a hybrid property, we need to
3278 # keep compatibility with the code which uses `group_name` field.
3277 # keep compatibility with the code which uses `group_name` field.
3279 result = super(RepoGroup, self).get_dict()
3278 result = super(RepoGroup, self).get_dict()
3280 result['group_name'] = result.pop('_group_name', None)
3279 result['group_name'] = result.pop('_group_name', None)
3281 result.pop('_changeset_cache', '')
3280 result.pop('_changeset_cache', '')
3282 return result
3281 return result
3283
3282
3284
3283
3285 class Permission(Base, BaseModel):
3284 class Permission(Base, BaseModel):
3286 __tablename__ = 'permissions'
3285 __tablename__ = 'permissions'
3287 __table_args__ = (
3286 __table_args__ = (
3288 Index('p_perm_name_idx', 'permission_name'),
3287 Index('p_perm_name_idx', 'permission_name'),
3289 base_table_args,
3288 base_table_args,
3290 )
3289 )
3291
3290
3292 PERMS = [
3291 PERMS = [
3293 ('hg.admin', _('RhodeCode Super Administrator')),
3292 ('hg.admin', _('RhodeCode Super Administrator')),
3294
3293
3295 ('repository.none', _('Repository no access')),
3294 ('repository.none', _('Repository no access')),
3296 ('repository.read', _('Repository read access')),
3295 ('repository.read', _('Repository read access')),
3297 ('repository.write', _('Repository write access')),
3296 ('repository.write', _('Repository write access')),
3298 ('repository.admin', _('Repository admin access')),
3297 ('repository.admin', _('Repository admin access')),
3299
3298
3300 ('group.none', _('Repository group no access')),
3299 ('group.none', _('Repository group no access')),
3301 ('group.read', _('Repository group read access')),
3300 ('group.read', _('Repository group read access')),
3302 ('group.write', _('Repository group write access')),
3301 ('group.write', _('Repository group write access')),
3303 ('group.admin', _('Repository group admin access')),
3302 ('group.admin', _('Repository group admin access')),
3304
3303
3305 ('usergroup.none', _('User group no access')),
3304 ('usergroup.none', _('User group no access')),
3306 ('usergroup.read', _('User group read access')),
3305 ('usergroup.read', _('User group read access')),
3307 ('usergroup.write', _('User group write access')),
3306 ('usergroup.write', _('User group write access')),
3308 ('usergroup.admin', _('User group admin access')),
3307 ('usergroup.admin', _('User group admin access')),
3309
3308
3310 ('branch.none', _('Branch no permissions')),
3309 ('branch.none', _('Branch no permissions')),
3311 ('branch.merge', _('Branch access by web merge')),
3310 ('branch.merge', _('Branch access by web merge')),
3312 ('branch.push', _('Branch access by push')),
3311 ('branch.push', _('Branch access by push')),
3313 ('branch.push_force', _('Branch access by push with force')),
3312 ('branch.push_force', _('Branch access by push with force')),
3314
3313
3315 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3314 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3316 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3315 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3317
3316
3318 ('hg.usergroup.create.false', _('User Group creation disabled')),
3317 ('hg.usergroup.create.false', _('User Group creation disabled')),
3319 ('hg.usergroup.create.true', _('User Group creation enabled')),
3318 ('hg.usergroup.create.true', _('User Group creation enabled')),
3320
3319
3321 ('hg.create.none', _('Repository creation disabled')),
3320 ('hg.create.none', _('Repository creation disabled')),
3322 ('hg.create.repository', _('Repository creation enabled')),
3321 ('hg.create.repository', _('Repository creation enabled')),
3323 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3322 ('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')),
3323 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3325
3324
3326 ('hg.fork.none', _('Repository forking disabled')),
3325 ('hg.fork.none', _('Repository forking disabled')),
3327 ('hg.fork.repository', _('Repository forking enabled')),
3326 ('hg.fork.repository', _('Repository forking enabled')),
3328
3327
3329 ('hg.register.none', _('Registration disabled')),
3328 ('hg.register.none', _('Registration disabled')),
3330 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3329 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3331 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3330 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3332
3331
3333 ('hg.password_reset.enabled', _('Password reset enabled')),
3332 ('hg.password_reset.enabled', _('Password reset enabled')),
3334 ('hg.password_reset.hidden', _('Password reset hidden')),
3333 ('hg.password_reset.hidden', _('Password reset hidden')),
3335 ('hg.password_reset.disabled', _('Password reset disabled')),
3334 ('hg.password_reset.disabled', _('Password reset disabled')),
3336
3335
3337 ('hg.extern_activate.manual', _('Manual activation of external account')),
3336 ('hg.extern_activate.manual', _('Manual activation of external account')),
3338 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3337 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3339
3338
3340 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3339 ('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')),
3340 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3342 ]
3341 ]
3343
3342
3344 # definition of system default permissions for DEFAULT user, created on
3343 # definition of system default permissions for DEFAULT user, created on
3345 # system setup
3344 # system setup
3346 DEFAULT_USER_PERMISSIONS = [
3345 DEFAULT_USER_PERMISSIONS = [
3347 # object perms
3346 # object perms
3348 'repository.read',
3347 'repository.read',
3349 'group.read',
3348 'group.read',
3350 'usergroup.read',
3349 'usergroup.read',
3351 # branch, for backward compat we need same value as before so forced pushed
3350 # branch, for backward compat we need same value as before so forced pushed
3352 'branch.push_force',
3351 'branch.push_force',
3353 # global
3352 # global
3354 'hg.create.repository',
3353 'hg.create.repository',
3355 'hg.repogroup.create.false',
3354 'hg.repogroup.create.false',
3356 'hg.usergroup.create.false',
3355 'hg.usergroup.create.false',
3357 'hg.create.write_on_repogroup.true',
3356 'hg.create.write_on_repogroup.true',
3358 'hg.fork.repository',
3357 'hg.fork.repository',
3359 'hg.register.manual_activate',
3358 'hg.register.manual_activate',
3360 'hg.password_reset.enabled',
3359 'hg.password_reset.enabled',
3361 'hg.extern_activate.auto',
3360 'hg.extern_activate.auto',
3362 'hg.inherit_default_perms.true',
3361 'hg.inherit_default_perms.true',
3363 ]
3362 ]
3364
3363
3365 # defines which permissions are more important higher the more important
3364 # defines which permissions are more important higher the more important
3366 # Weight defines which permissions are more important.
3365 # Weight defines which permissions are more important.
3367 # The higher number the more important.
3366 # The higher number the more important.
3368 PERM_WEIGHTS = {
3367 PERM_WEIGHTS = {
3369 'repository.none': 0,
3368 'repository.none': 0,
3370 'repository.read': 1,
3369 'repository.read': 1,
3371 'repository.write': 3,
3370 'repository.write': 3,
3372 'repository.admin': 4,
3371 'repository.admin': 4,
3373
3372
3374 'group.none': 0,
3373 'group.none': 0,
3375 'group.read': 1,
3374 'group.read': 1,
3376 'group.write': 3,
3375 'group.write': 3,
3377 'group.admin': 4,
3376 'group.admin': 4,
3378
3377
3379 'usergroup.none': 0,
3378 'usergroup.none': 0,
3380 'usergroup.read': 1,
3379 'usergroup.read': 1,
3381 'usergroup.write': 3,
3380 'usergroup.write': 3,
3382 'usergroup.admin': 4,
3381 'usergroup.admin': 4,
3383
3382
3384 'branch.none': 0,
3383 'branch.none': 0,
3385 'branch.merge': 1,
3384 'branch.merge': 1,
3386 'branch.push': 3,
3385 'branch.push': 3,
3387 'branch.push_force': 4,
3386 'branch.push_force': 4,
3388
3387
3389 'hg.repogroup.create.false': 0,
3388 'hg.repogroup.create.false': 0,
3390 'hg.repogroup.create.true': 1,
3389 'hg.repogroup.create.true': 1,
3391
3390
3392 'hg.usergroup.create.false': 0,
3391 'hg.usergroup.create.false': 0,
3393 'hg.usergroup.create.true': 1,
3392 'hg.usergroup.create.true': 1,
3394
3393
3395 'hg.fork.none': 0,
3394 'hg.fork.none': 0,
3396 'hg.fork.repository': 1,
3395 'hg.fork.repository': 1,
3397 'hg.create.none': 0,
3396 'hg.create.none': 0,
3398 'hg.create.repository': 1
3397 'hg.create.repository': 1
3399 }
3398 }
3400
3399
3401 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3400 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)
3401 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)
3402 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3404
3403
3405 def __repr__(self):
3404 def __repr__(self):
3406 return "<%s('%s:%s')>" % (
3405 return "<%s('%s:%s')>" % (
3407 self.cls_name, self.permission_id, self.permission_name
3406 self.cls_name, self.permission_id, self.permission_name
3408 )
3407 )
3409
3408
3410 @classmethod
3409 @classmethod
3411 def get_by_key(cls, key):
3410 def get_by_key(cls, key):
3412 return cls.query().filter(cls.permission_name == key).scalar()
3411 return cls.query().filter(cls.permission_name == key).scalar()
3413
3412
3414 @classmethod
3413 @classmethod
3415 def get_default_repo_perms(cls, user_id, repo_id=None):
3414 def get_default_repo_perms(cls, user_id, repo_id=None):
3416 q = Session().query(UserRepoToPerm, Repository, Permission)\
3415 q = Session().query(UserRepoToPerm, Repository, Permission)\
3417 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3416 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3418 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3417 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3419 .filter(UserRepoToPerm.user_id == user_id)
3418 .filter(UserRepoToPerm.user_id == user_id)
3420 if repo_id:
3419 if repo_id:
3421 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3420 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3422 return q.all()
3421 return q.all()
3423
3422
3424 @classmethod
3423 @classmethod
3425 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3424 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3426 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3425 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3427 .join(
3426 .join(
3428 Permission,
3427 Permission,
3429 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3428 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3430 .join(
3429 .join(
3431 UserRepoToPerm,
3430 UserRepoToPerm,
3432 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3431 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3433 .filter(UserRepoToPerm.user_id == user_id)
3432 .filter(UserRepoToPerm.user_id == user_id)
3434
3433
3435 if repo_id:
3434 if repo_id:
3436 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3435 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3437 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3436 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3438
3437
3439 @classmethod
3438 @classmethod
3440 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3439 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3441 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3440 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3442 .join(
3441 .join(
3443 Permission,
3442 Permission,
3444 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3443 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3445 .join(
3444 .join(
3446 Repository,
3445 Repository,
3447 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3446 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3448 .join(
3447 .join(
3449 UserGroup,
3448 UserGroup,
3450 UserGroupRepoToPerm.users_group_id ==
3449 UserGroupRepoToPerm.users_group_id ==
3451 UserGroup.users_group_id)\
3450 UserGroup.users_group_id)\
3452 .join(
3451 .join(
3453 UserGroupMember,
3452 UserGroupMember,
3454 UserGroupRepoToPerm.users_group_id ==
3453 UserGroupRepoToPerm.users_group_id ==
3455 UserGroupMember.users_group_id)\
3454 UserGroupMember.users_group_id)\
3456 .filter(
3455 .filter(
3457 UserGroupMember.user_id == user_id,
3456 UserGroupMember.user_id == user_id,
3458 UserGroup.users_group_active == true())
3457 UserGroup.users_group_active == true())
3459 if repo_id:
3458 if repo_id:
3460 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3459 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3461 return q.all()
3460 return q.all()
3462
3461
3463 @classmethod
3462 @classmethod
3464 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3463 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3465 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3464 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3466 .join(
3465 .join(
3467 Permission,
3466 Permission,
3468 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3467 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3469 .join(
3468 .join(
3470 UserGroupRepoToPerm,
3469 UserGroupRepoToPerm,
3471 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3470 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3472 .join(
3471 .join(
3473 UserGroup,
3472 UserGroup,
3474 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3473 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3475 .join(
3474 .join(
3476 UserGroupMember,
3475 UserGroupMember,
3477 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3476 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3478 .filter(
3477 .filter(
3479 UserGroupMember.user_id == user_id,
3478 UserGroupMember.user_id == user_id,
3480 UserGroup.users_group_active == true())
3479 UserGroup.users_group_active == true())
3481
3480
3482 if repo_id:
3481 if repo_id:
3483 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3482 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3484 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3483 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3485
3484
3486 @classmethod
3485 @classmethod
3487 def get_default_group_perms(cls, user_id, repo_group_id=None):
3486 def get_default_group_perms(cls, user_id, repo_group_id=None):
3488 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3487 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3489 .join(
3488 .join(
3490 Permission,
3489 Permission,
3491 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3490 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3492 .join(
3491 .join(
3493 RepoGroup,
3492 RepoGroup,
3494 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3493 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3495 .filter(UserRepoGroupToPerm.user_id == user_id)
3494 .filter(UserRepoGroupToPerm.user_id == user_id)
3496 if repo_group_id:
3495 if repo_group_id:
3497 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3496 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3498 return q.all()
3497 return q.all()
3499
3498
3500 @classmethod
3499 @classmethod
3501 def get_default_group_perms_from_user_group(
3500 def get_default_group_perms_from_user_group(
3502 cls, user_id, repo_group_id=None):
3501 cls, user_id, repo_group_id=None):
3503 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3502 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3504 .join(
3503 .join(
3505 Permission,
3504 Permission,
3506 UserGroupRepoGroupToPerm.permission_id ==
3505 UserGroupRepoGroupToPerm.permission_id ==
3507 Permission.permission_id)\
3506 Permission.permission_id)\
3508 .join(
3507 .join(
3509 RepoGroup,
3508 RepoGroup,
3510 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3509 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3511 .join(
3510 .join(
3512 UserGroup,
3511 UserGroup,
3513 UserGroupRepoGroupToPerm.users_group_id ==
3512 UserGroupRepoGroupToPerm.users_group_id ==
3514 UserGroup.users_group_id)\
3513 UserGroup.users_group_id)\
3515 .join(
3514 .join(
3516 UserGroupMember,
3515 UserGroupMember,
3517 UserGroupRepoGroupToPerm.users_group_id ==
3516 UserGroupRepoGroupToPerm.users_group_id ==
3518 UserGroupMember.users_group_id)\
3517 UserGroupMember.users_group_id)\
3519 .filter(
3518 .filter(
3520 UserGroupMember.user_id == user_id,
3519 UserGroupMember.user_id == user_id,
3521 UserGroup.users_group_active == true())
3520 UserGroup.users_group_active == true())
3522 if repo_group_id:
3521 if repo_group_id:
3523 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3522 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3524 return q.all()
3523 return q.all()
3525
3524
3526 @classmethod
3525 @classmethod
3527 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3526 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3528 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3527 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3529 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3528 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3530 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3529 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3531 .filter(UserUserGroupToPerm.user_id == user_id)
3530 .filter(UserUserGroupToPerm.user_id == user_id)
3532 if user_group_id:
3531 if user_group_id:
3533 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3532 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3534 return q.all()
3533 return q.all()
3535
3534
3536 @classmethod
3535 @classmethod
3537 def get_default_user_group_perms_from_user_group(
3536 def get_default_user_group_perms_from_user_group(
3538 cls, user_id, user_group_id=None):
3537 cls, user_id, user_group_id=None):
3539 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3538 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3540 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3539 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3541 .join(
3540 .join(
3542 Permission,
3541 Permission,
3543 UserGroupUserGroupToPerm.permission_id ==
3542 UserGroupUserGroupToPerm.permission_id ==
3544 Permission.permission_id)\
3543 Permission.permission_id)\
3545 .join(
3544 .join(
3546 TargetUserGroup,
3545 TargetUserGroup,
3547 UserGroupUserGroupToPerm.target_user_group_id ==
3546 UserGroupUserGroupToPerm.target_user_group_id ==
3548 TargetUserGroup.users_group_id)\
3547 TargetUserGroup.users_group_id)\
3549 .join(
3548 .join(
3550 UserGroup,
3549 UserGroup,
3551 UserGroupUserGroupToPerm.user_group_id ==
3550 UserGroupUserGroupToPerm.user_group_id ==
3552 UserGroup.users_group_id)\
3551 UserGroup.users_group_id)\
3553 .join(
3552 .join(
3554 UserGroupMember,
3553 UserGroupMember,
3555 UserGroupUserGroupToPerm.user_group_id ==
3554 UserGroupUserGroupToPerm.user_group_id ==
3556 UserGroupMember.users_group_id)\
3555 UserGroupMember.users_group_id)\
3557 .filter(
3556 .filter(
3558 UserGroupMember.user_id == user_id,
3557 UserGroupMember.user_id == user_id,
3559 UserGroup.users_group_active == true())
3558 UserGroup.users_group_active == true())
3560 if user_group_id:
3559 if user_group_id:
3561 q = q.filter(
3560 q = q.filter(
3562 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3561 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3563
3562
3564 return q.all()
3563 return q.all()
3565
3564
3566
3565
3567 class UserRepoToPerm(Base, BaseModel):
3566 class UserRepoToPerm(Base, BaseModel):
3568 __tablename__ = 'repo_to_perm'
3567 __tablename__ = 'repo_to_perm'
3569 __table_args__ = (
3568 __table_args__ = (
3570 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3569 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3571 base_table_args
3570 base_table_args
3572 )
3571 )
3573
3572
3574 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3573 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)
3574 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)
3575 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)
3576 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3578
3577
3579 user = relationship('User', back_populates="repo_to_perm")
3578 user = relationship('User', back_populates="repo_to_perm")
3580 repository = relationship('Repository', back_populates="repo_to_perm")
3579 repository = relationship('Repository', back_populates="repo_to_perm")
3581 permission = relationship('Permission')
3580 permission = relationship('Permission')
3582
3581
3583 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3582 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3584
3583
3585 @classmethod
3584 @classmethod
3586 def create(cls, user, repository, permission):
3585 def create(cls, user, repository, permission):
3587 n = cls()
3586 n = cls()
3588 n.user = user
3587 n.user = user
3589 n.repository = repository
3588 n.repository = repository
3590 n.permission = permission
3589 n.permission = permission
3591 Session().add(n)
3590 Session().add(n)
3592 return n
3591 return n
3593
3592
3594 def __repr__(self):
3593 def __repr__(self):
3595 return f'<{self.user} => {self.repository} >'
3594 return f'<{self.user} => {self.repository} >'
3596
3595
3597
3596
3598 class UserUserGroupToPerm(Base, BaseModel):
3597 class UserUserGroupToPerm(Base, BaseModel):
3599 __tablename__ = 'user_user_group_to_perm'
3598 __tablename__ = 'user_user_group_to_perm'
3600 __table_args__ = (
3599 __table_args__ = (
3601 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3600 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3602 base_table_args
3601 base_table_args
3603 )
3602 )
3604
3603
3605 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3604 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)
3605 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)
3606 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)
3607 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3609
3608
3610 user = relationship('User', back_populates='user_group_to_perm')
3609 user = relationship('User', back_populates='user_group_to_perm')
3611 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3610 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3612 permission = relationship('Permission')
3611 permission = relationship('Permission')
3613
3612
3614 @classmethod
3613 @classmethod
3615 def create(cls, user, user_group, permission):
3614 def create(cls, user, user_group, permission):
3616 n = cls()
3615 n = cls()
3617 n.user = user
3616 n.user = user
3618 n.user_group = user_group
3617 n.user_group = user_group
3619 n.permission = permission
3618 n.permission = permission
3620 Session().add(n)
3619 Session().add(n)
3621 return n
3620 return n
3622
3621
3623 def __repr__(self):
3622 def __repr__(self):
3624 return f'<{self.user} => {self.user_group} >'
3623 return f'<{self.user} => {self.user_group} >'
3625
3624
3626
3625
3627 class UserToPerm(Base, BaseModel):
3626 class UserToPerm(Base, BaseModel):
3628 __tablename__ = 'user_to_perm'
3627 __tablename__ = 'user_to_perm'
3629 __table_args__ = (
3628 __table_args__ = (
3630 UniqueConstraint('user_id', 'permission_id'),
3629 UniqueConstraint('user_id', 'permission_id'),
3631 base_table_args
3630 base_table_args
3632 )
3631 )
3633
3632
3634 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3633 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)
3634 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)
3635 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3637
3636
3638 user = relationship('User', back_populates='user_perms')
3637 user = relationship('User', back_populates='user_perms')
3639 permission = relationship('Permission', lazy='joined')
3638 permission = relationship('Permission', lazy='joined')
3640
3639
3641 def __repr__(self):
3640 def __repr__(self):
3642 return f'<{self.user} => {self.permission} >'
3641 return f'<{self.user} => {self.permission} >'
3643
3642
3644
3643
3645 class UserGroupRepoToPerm(Base, BaseModel):
3644 class UserGroupRepoToPerm(Base, BaseModel):
3646 __tablename__ = 'users_group_repo_to_perm'
3645 __tablename__ = 'users_group_repo_to_perm'
3647 __table_args__ = (
3646 __table_args__ = (
3648 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3647 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3649 base_table_args
3648 base_table_args
3650 )
3649 )
3651
3650
3652 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3651 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)
3652 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)
3653 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)
3654 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3656
3655
3657 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3656 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3658 permission = relationship('Permission')
3657 permission = relationship('Permission')
3659 repository = relationship('Repository', back_populates='users_group_to_perm')
3658 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')
3659 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3661
3660
3662 @classmethod
3661 @classmethod
3663 def create(cls, users_group, repository, permission):
3662 def create(cls, users_group, repository, permission):
3664 n = cls()
3663 n = cls()
3665 n.users_group = users_group
3664 n.users_group = users_group
3666 n.repository = repository
3665 n.repository = repository
3667 n.permission = permission
3666 n.permission = permission
3668 Session().add(n)
3667 Session().add(n)
3669 return n
3668 return n
3670
3669
3671 def __repr__(self):
3670 def __repr__(self):
3672 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3671 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3673
3672
3674
3673
3675 class UserGroupUserGroupToPerm(Base, BaseModel):
3674 class UserGroupUserGroupToPerm(Base, BaseModel):
3676 __tablename__ = 'user_group_user_group_to_perm'
3675 __tablename__ = 'user_group_user_group_to_perm'
3677 __table_args__ = (
3676 __table_args__ = (
3678 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3677 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3679 CheckConstraint('target_user_group_id != user_group_id'),
3678 CheckConstraint('target_user_group_id != user_group_id'),
3680 base_table_args
3679 base_table_args
3681 )
3680 )
3682
3681
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)
3682 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)
3683 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)
3684 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)
3685 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3687
3686
3688 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3687 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')
3688 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3690 permission = relationship('Permission')
3689 permission = relationship('Permission')
3691
3690
3692 @classmethod
3691 @classmethod
3693 def create(cls, target_user_group, user_group, permission):
3692 def create(cls, target_user_group, user_group, permission):
3694 n = cls()
3693 n = cls()
3695 n.target_user_group = target_user_group
3694 n.target_user_group = target_user_group
3696 n.user_group = user_group
3695 n.user_group = user_group
3697 n.permission = permission
3696 n.permission = permission
3698 Session().add(n)
3697 Session().add(n)
3699 return n
3698 return n
3700
3699
3701 def __repr__(self):
3700 def __repr__(self):
3702 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3701 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3703
3702
3704
3703
3705 class UserGroupToPerm(Base, BaseModel):
3704 class UserGroupToPerm(Base, BaseModel):
3706 __tablename__ = 'users_group_to_perm'
3705 __tablename__ = 'users_group_to_perm'
3707 __table_args__ = (
3706 __table_args__ = (
3708 UniqueConstraint('users_group_id', 'permission_id',),
3707 UniqueConstraint('users_group_id', 'permission_id',),
3709 base_table_args
3708 base_table_args
3710 )
3709 )
3711
3710
3712 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3711 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)
3712 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)
3713 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3715
3714
3716 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3715 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3717 permission = relationship('Permission')
3716 permission = relationship('Permission')
3718
3717
3719
3718
3720 class UserRepoGroupToPerm(Base, BaseModel):
3719 class UserRepoGroupToPerm(Base, BaseModel):
3721 __tablename__ = 'user_repo_group_to_perm'
3720 __tablename__ = 'user_repo_group_to_perm'
3722 __table_args__ = (
3721 __table_args__ = (
3723 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3722 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3724 base_table_args
3723 base_table_args
3725 )
3724 )
3726
3725
3727 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3726 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)
3727 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)
3728 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)
3729 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3731
3730
3732 user = relationship('User', back_populates='repo_group_to_perm')
3731 user = relationship('User', back_populates='repo_group_to_perm')
3733 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3732 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3734 permission = relationship('Permission')
3733 permission = relationship('Permission')
3735
3734
3736 @classmethod
3735 @classmethod
3737 def create(cls, user, repository_group, permission):
3736 def create(cls, user, repository_group, permission):
3738 n = cls()
3737 n = cls()
3739 n.user = user
3738 n.user = user
3740 n.group = repository_group
3739 n.group = repository_group
3741 n.permission = permission
3740 n.permission = permission
3742 Session().add(n)
3741 Session().add(n)
3743 return n
3742 return n
3744
3743
3745
3744
3746 class UserGroupRepoGroupToPerm(Base, BaseModel):
3745 class UserGroupRepoGroupToPerm(Base, BaseModel):
3747 __tablename__ = 'users_group_repo_group_to_perm'
3746 __tablename__ = 'users_group_repo_group_to_perm'
3748 __table_args__ = (
3747 __table_args__ = (
3749 UniqueConstraint('users_group_id', 'group_id'),
3748 UniqueConstraint('users_group_id', 'group_id'),
3750 base_table_args
3749 base_table_args
3751 )
3750 )
3752
3751
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)
3752 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)
3753 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)
3754 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)
3755 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3757
3756
3758 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3757 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3759 permission = relationship('Permission')
3758 permission = relationship('Permission')
3760 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3759 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3761
3760
3762 @classmethod
3761 @classmethod
3763 def create(cls, user_group, repository_group, permission):
3762 def create(cls, user_group, repository_group, permission):
3764 n = cls()
3763 n = cls()
3765 n.users_group = user_group
3764 n.users_group = user_group
3766 n.group = repository_group
3765 n.group = repository_group
3767 n.permission = permission
3766 n.permission = permission
3768 Session().add(n)
3767 Session().add(n)
3769 return n
3768 return n
3770
3769
3771 def __repr__(self):
3770 def __repr__(self):
3772 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3771 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3773
3772
3774
3773
3775 class Statistics(Base, BaseModel):
3774 class Statistics(Base, BaseModel):
3776 __tablename__ = 'statistics'
3775 __tablename__ = 'statistics'
3777 __table_args__ = (
3776 __table_args__ = (
3778 base_table_args
3777 base_table_args
3779 )
3778 )
3780
3779
3781 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3780 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)
3781 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)
3782 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3784 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3783 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3785 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3784 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3786 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3785 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3787
3786
3788 repository = relationship('Repository', single_parent=True, viewonly=True)
3787 repository = relationship('Repository', single_parent=True, viewonly=True)
3789
3788
3790
3789
3791 class UserFollowing(Base, BaseModel):
3790 class UserFollowing(Base, BaseModel):
3792 __tablename__ = 'user_followings'
3791 __tablename__ = 'user_followings'
3793 __table_args__ = (
3792 __table_args__ = (
3794 UniqueConstraint('user_id', 'follows_repository_id'),
3793 UniqueConstraint('user_id', 'follows_repository_id'),
3795 UniqueConstraint('user_id', 'follows_user_id'),
3794 UniqueConstraint('user_id', 'follows_user_id'),
3796 base_table_args
3795 base_table_args
3797 )
3796 )
3798
3797
3799 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3798 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)
3799 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)
3800 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)
3801 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)
3802 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3804
3803
3805 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3804 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3806
3805
3807 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3806 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')
3807 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3809
3808
3810 @classmethod
3809 @classmethod
3811 def get_repo_followers(cls, repo_id):
3810 def get_repo_followers(cls, repo_id):
3812 return cls.query().filter(cls.follows_repo_id == repo_id)
3811 return cls.query().filter(cls.follows_repo_id == repo_id)
3813
3812
3814
3813
3815 class CacheKey(Base, BaseModel):
3814 class CacheKey(Base, BaseModel):
3816 __tablename__ = 'cache_invalidation'
3815 __tablename__ = 'cache_invalidation'
3817 __table_args__ = (
3816 __table_args__ = (
3818 UniqueConstraint('cache_key'),
3817 UniqueConstraint('cache_key'),
3819 Index('key_idx', 'cache_key'),
3818 Index('key_idx', 'cache_key'),
3820 Index('cache_args_idx', 'cache_args'),
3819 Index('cache_args_idx', 'cache_args'),
3821 base_table_args,
3820 base_table_args,
3822 )
3821 )
3823
3822
3824 CACHE_TYPE_FEED = 'FEED'
3823 CACHE_TYPE_FEED = 'FEED'
3825
3824
3826 # namespaces used to register process/thread aware caches
3825 # namespaces used to register process/thread aware caches
3827 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3826 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3828
3827
3829 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3828 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)
3829 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)
3830 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)
3831 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)
3832 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3834
3833
3835 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3834 def __init__(self, cache_key, cache_args='', cache_state_uid=None, cache_active=False):
3836 self.cache_key = cache_key
3835 self.cache_key = cache_key
3837 self.cache_args = cache_args
3836 self.cache_args = cache_args
3838 self.cache_active = cache_active
3837 self.cache_active = cache_active
3839 # first key should be same for all entries, since all workers should share it
3838 # 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()
3839 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3841
3840
3842 def __repr__(self):
3841 def __repr__(self):
3843 return "<%s('%s:%s[%s]')>" % (
3842 return "<%s('%s:%s[%s]')>" % (
3844 self.cls_name,
3843 self.cls_name,
3845 self.cache_id, self.cache_key, self.cache_active)
3844 self.cache_id, self.cache_key, self.cache_active)
3846
3845
3847 def _cache_key_partition(self):
3846 def _cache_key_partition(self):
3848 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3847 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3849 return prefix, repo_name, suffix
3848 return prefix, repo_name, suffix
3850
3849
3851 def get_prefix(self):
3850 def get_prefix(self):
3852 """
3851 """
3853 Try to extract prefix from existing cache key. The key could consist
3852 Try to extract prefix from existing cache key. The key could consist
3854 of prefix, repo_name, suffix
3853 of prefix, repo_name, suffix
3855 """
3854 """
3856 # this returns prefix, repo_name, suffix
3855 # this returns prefix, repo_name, suffix
3857 return self._cache_key_partition()[0]
3856 return self._cache_key_partition()[0]
3858
3857
3859 def get_suffix(self):
3858 def get_suffix(self):
3860 """
3859 """
3861 get suffix that might have been used in _get_cache_key to
3860 get suffix that might have been used in _get_cache_key to
3862 generate self.cache_key. Only used for informational purposes
3861 generate self.cache_key. Only used for informational purposes
3863 in repo_edit.mako.
3862 in repo_edit.mako.
3864 """
3863 """
3865 # prefix, repo_name, suffix
3864 # prefix, repo_name, suffix
3866 return self._cache_key_partition()[2]
3865 return self._cache_key_partition()[2]
3867
3866
3868 @classmethod
3867 @classmethod
3869 def generate_new_state_uid(cls, based_on=None):
3868 def generate_new_state_uid(cls, based_on=None):
3870 if based_on:
3869 if based_on:
3871 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3870 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3872 else:
3871 else:
3873 return str(uuid.uuid4())
3872 return str(uuid.uuid4())
3874
3873
3875 @classmethod
3874 @classmethod
3876 def delete_all_cache(cls):
3875 def delete_all_cache(cls):
3877 """
3876 """
3878 Delete all cache keys from database.
3877 Delete all cache keys from database.
3879 Should only be run when all instances are down and all entries
3878 Should only be run when all instances are down and all entries
3880 thus stale.
3879 thus stale.
3881 """
3880 """
3882 cls.query().delete()
3881 cls.query().delete()
3883 Session().commit()
3882 Session().commit()
3884
3883
3885 @classmethod
3884 @classmethod
3886 def set_invalidate(cls, cache_uid, delete=False):
3885 def set_invalidate(cls, cache_uid, delete=False):
3887 """
3886 """
3888 Mark all caches of a repo as invalid in the database.
3887 Mark all caches of a repo as invalid in the database.
3889 """
3888 """
3890 try:
3889 try:
3891 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3890 qry = Session().query(cls).filter(cls.cache_key == cache_uid)
3892 if delete:
3891 if delete:
3893 qry.delete()
3892 qry.delete()
3894 log.debug('cache objects deleted for cache args %s',
3893 log.debug('cache objects deleted for cache args %s',
3895 safe_str(cache_uid))
3894 safe_str(cache_uid))
3896 else:
3895 else:
3897 new_uid = cls.generate_new_state_uid()
3896 new_uid = cls.generate_new_state_uid()
3898 qry.update({"cache_state_uid": new_uid,
3897 qry.update({"cache_state_uid": new_uid,
3899 "cache_args": f"repo_state:{time.time()}"})
3898 "cache_args": f"repo_state:{time.time()}"})
3900 log.debug('cache object %s set new UID %s',
3899 log.debug('cache object %s set new UID %s',
3901 safe_str(cache_uid), new_uid)
3900 safe_str(cache_uid), new_uid)
3902
3901
3903 Session().commit()
3902 Session().commit()
3904 except Exception:
3903 except Exception:
3905 log.exception(
3904 log.exception(
3906 'Cache key invalidation failed for cache args %s',
3905 'Cache key invalidation failed for cache args %s',
3907 safe_str(cache_uid))
3906 safe_str(cache_uid))
3908 Session().rollback()
3907 Session().rollback()
3909
3908
3910 @classmethod
3909 @classmethod
3911 def get_active_cache(cls, cache_key):
3910 def get_active_cache(cls, cache_key):
3912 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3911 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3913 if inv_obj:
3912 if inv_obj:
3914 return inv_obj
3913 return inv_obj
3915 return None
3914 return None
3916
3915
3917 @classmethod
3916 @classmethod
3918 def get_namespace_map(cls, namespace):
3917 def get_namespace_map(cls, namespace):
3919 return {
3918 return {
3920 x.cache_key: x
3919 x.cache_key: x
3921 for x in cls.query().filter(cls.cache_args == namespace)}
3920 for x in cls.query().filter(cls.cache_args == namespace)}
3922
3921
3923
3922
3924 class ChangesetComment(Base, BaseModel):
3923 class ChangesetComment(Base, BaseModel):
3925 __tablename__ = 'changeset_comments'
3924 __tablename__ = 'changeset_comments'
3926 __table_args__ = (
3925 __table_args__ = (
3927 Index('cc_revision_idx', 'revision'),
3926 Index('cc_revision_idx', 'revision'),
3928 base_table_args,
3927 base_table_args,
3929 )
3928 )
3930
3929
3931 COMMENT_OUTDATED = 'comment_outdated'
3930 COMMENT_OUTDATED = 'comment_outdated'
3932 COMMENT_TYPE_NOTE = 'note'
3931 COMMENT_TYPE_NOTE = 'note'
3933 COMMENT_TYPE_TODO = 'todo'
3932 COMMENT_TYPE_TODO = 'todo'
3934 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3933 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3935
3934
3936 OP_IMMUTABLE = 'immutable'
3935 OP_IMMUTABLE = 'immutable'
3937 OP_CHANGEABLE = 'changeable'
3936 OP_CHANGEABLE = 'changeable'
3938
3937
3939 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3938 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3940 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3939 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3941 revision = Column('revision', String(40), nullable=True)
3940 revision = Column('revision', String(40), nullable=True)
3942 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3941 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)
3942 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)
3943 line_no = Column('line_no', Unicode(10), nullable=True)
3945 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3944 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3946 f_path = Column('f_path', Unicode(1000), nullable=True)
3945 f_path = Column('f_path', Unicode(1000), nullable=True)
3947 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3946 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3948 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3947 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)
3948 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)
3949 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3951 renderer = Column('renderer', Unicode(64), nullable=True)
3950 renderer = Column('renderer', Unicode(64), nullable=True)
3952 display_state = Column('display_state', Unicode(128), nullable=True)
3951 display_state = Column('display_state', Unicode(128), nullable=True)
3953 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3952 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3954 draft = Column('draft', Boolean(), nullable=True, default=False)
3953 draft = Column('draft', Boolean(), nullable=True, default=False)
3955
3954
3956 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3955 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)
3956 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3958
3957
3959 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3958 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3960 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3959 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3961
3960
3962 author = relationship('User', lazy='select', back_populates='user_comments')
3961 author = relationship('User', lazy='select', back_populates='user_comments')
3963 repo = relationship('Repository', back_populates='comments')
3962 repo = relationship('Repository', back_populates='comments')
3964 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3963 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3965 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3964 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3966 pull_request_version = relationship('PullRequestVersion', lazy='select')
3965 pull_request_version = relationship('PullRequestVersion', lazy='select')
3967 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3966 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3968
3967
3969 @classmethod
3968 @classmethod
3970 def get_users(cls, revision=None, pull_request_id=None):
3969 def get_users(cls, revision=None, pull_request_id=None):
3971 """
3970 """
3972 Returns user associated with this ChangesetComment. ie those
3971 Returns user associated with this ChangesetComment. ie those
3973 who actually commented
3972 who actually commented
3974
3973
3975 :param cls:
3974 :param cls:
3976 :param revision:
3975 :param revision:
3977 """
3976 """
3978 q = Session().query(User).join(ChangesetComment.author)
3977 q = Session().query(User).join(ChangesetComment.author)
3979 if revision:
3978 if revision:
3980 q = q.filter(cls.revision == revision)
3979 q = q.filter(cls.revision == revision)
3981 elif pull_request_id:
3980 elif pull_request_id:
3982 q = q.filter(cls.pull_request_id == pull_request_id)
3981 q = q.filter(cls.pull_request_id == pull_request_id)
3983 return q.all()
3982 return q.all()
3984
3983
3985 @classmethod
3984 @classmethod
3986 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3985 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3987 if pr_version is None:
3986 if pr_version is None:
3988 return 0
3987 return 0
3989
3988
3990 if versions is not None:
3989 if versions is not None:
3991 num_versions = [x.pull_request_version_id for x in versions]
3990 num_versions = [x.pull_request_version_id for x in versions]
3992
3991
3993 num_versions = num_versions or []
3992 num_versions = num_versions or []
3994 try:
3993 try:
3995 return num_versions.index(pr_version) + 1
3994 return num_versions.index(pr_version) + 1
3996 except (IndexError, ValueError):
3995 except (IndexError, ValueError):
3997 return 0
3996 return 0
3998
3997
3999 @property
3998 @property
4000 def outdated(self):
3999 def outdated(self):
4001 return self.display_state == self.COMMENT_OUTDATED
4000 return self.display_state == self.COMMENT_OUTDATED
4002
4001
4003 @property
4002 @property
4004 def outdated_js(self):
4003 def outdated_js(self):
4005 return str_json(self.display_state == self.COMMENT_OUTDATED)
4004 return str_json(self.display_state == self.COMMENT_OUTDATED)
4006
4005
4007 @property
4006 @property
4008 def immutable(self):
4007 def immutable(self):
4009 return self.immutable_state == self.OP_IMMUTABLE
4008 return self.immutable_state == self.OP_IMMUTABLE
4010
4009
4011 def outdated_at_version(self, version: int) -> bool:
4010 def outdated_at_version(self, version: int) -> bool:
4012 """
4011 """
4013 Checks if comment is outdated for given pull request version
4012 Checks if comment is outdated for given pull request version
4014 """
4013 """
4015
4014
4016 def version_check():
4015 def version_check():
4017 return self.pull_request_version_id and self.pull_request_version_id != version
4016 return self.pull_request_version_id and self.pull_request_version_id != version
4018
4017
4019 if self.is_inline:
4018 if self.is_inline:
4020 return self.outdated and version_check()
4019 return self.outdated and version_check()
4021 else:
4020 else:
4022 # general comments don't have .outdated set, also latest don't have a version
4021 # general comments don't have .outdated set, also latest don't have a version
4023 return version_check()
4022 return version_check()
4024
4023
4025 def outdated_at_version_js(self, version):
4024 def outdated_at_version_js(self, version):
4026 """
4025 """
4027 Checks if comment is outdated for given pull request version
4026 Checks if comment is outdated for given pull request version
4028 """
4027 """
4029 return str_json(self.outdated_at_version(version))
4028 return str_json(self.outdated_at_version(version))
4030
4029
4031 def older_than_version(self, version: int) -> bool:
4030 def older_than_version(self, version: int) -> bool:
4032 """
4031 """
4033 Checks if comment is made from a previous version than given.
4032 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.
4033 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
4035 """
4034 """
4036
4035
4037 # If version is None, return False as the current version cannot be less than None
4036 # If version is None, return False as the current version cannot be less than None
4038 if version is None:
4037 if version is None:
4039 return False
4038 return False
4040
4039
4041 # Ensure that the version is an integer to prevent TypeError on comparison
4040 # Ensure that the version is an integer to prevent TypeError on comparison
4042 if not isinstance(version, int):
4041 if not isinstance(version, int):
4043 raise ValueError("The provided version must be an integer.")
4042 raise ValueError("The provided version must be an integer.")
4044
4043
4045 # Initialize current version to 0 or pull_request_version_id if it's available
4044 # Initialize current version to 0 or pull_request_version_id if it's available
4046 cur_ver = 0
4045 cur_ver = 0
4047 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
4046 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
4047 cur_ver = self.pull_request_version.pull_request_version_id
4049
4048
4050 # Return True if the current version is less than the given version
4049 # Return True if the current version is less than the given version
4051 return cur_ver < version
4050 return cur_ver < version
4052
4051
4053 def older_than_version_js(self, version):
4052 def older_than_version_js(self, version):
4054 """
4053 """
4055 Checks if comment is made from previous version than given
4054 Checks if comment is made from previous version than given
4056 """
4055 """
4057 return str_json(self.older_than_version(version))
4056 return str_json(self.older_than_version(version))
4058
4057
4059 @property
4058 @property
4060 def commit_id(self):
4059 def commit_id(self):
4061 """New style naming to stop using .revision"""
4060 """New style naming to stop using .revision"""
4062 return self.revision
4061 return self.revision
4063
4062
4064 @property
4063 @property
4065 def resolved(self):
4064 def resolved(self):
4066 return self.resolved_by[0] if self.resolved_by else None
4065 return self.resolved_by[0] if self.resolved_by else None
4067
4066
4068 @property
4067 @property
4069 def is_todo(self):
4068 def is_todo(self):
4070 return self.comment_type == self.COMMENT_TYPE_TODO
4069 return self.comment_type == self.COMMENT_TYPE_TODO
4071
4070
4072 @property
4071 @property
4073 def is_inline(self):
4072 def is_inline(self):
4074 if self.line_no and self.f_path:
4073 if self.line_no and self.f_path:
4075 return True
4074 return True
4076 return False
4075 return False
4077
4076
4078 @property
4077 @property
4079 def last_version(self):
4078 def last_version(self):
4080 version = 0
4079 version = 0
4081 if self.history:
4080 if self.history:
4082 version = self.history[-1].version
4081 version = self.history[-1].version
4083 return version
4082 return version
4084
4083
4085 def get_index_version(self, versions):
4084 def get_index_version(self, versions):
4086 return self.get_index_from_version(
4085 return self.get_index_from_version(
4087 self.pull_request_version_id, versions)
4086 self.pull_request_version_id, versions)
4088
4087
4089 @property
4088 @property
4090 def review_status(self):
4089 def review_status(self):
4091 if self.status_change:
4090 if self.status_change:
4092 return self.status_change[0].status
4091 return self.status_change[0].status
4093
4092
4094 @property
4093 @property
4095 def review_status_lbl(self):
4094 def review_status_lbl(self):
4096 if self.status_change:
4095 if self.status_change:
4097 return self.status_change[0].status_lbl
4096 return self.status_change[0].status_lbl
4098
4097
4099 def __repr__(self):
4098 def __repr__(self):
4100 if self.comment_id:
4099 if self.comment_id:
4101 return f'<DB:Comment #{self.comment_id}>'
4100 return f'<DB:Comment #{self.comment_id}>'
4102 else:
4101 else:
4103 return f'<DB:Comment at {id(self)!r}>'
4102 return f'<DB:Comment at {id(self)!r}>'
4104
4103
4105 def get_api_data(self):
4104 def get_api_data(self):
4106 comment = self
4105 comment = self
4107
4106
4108 data = {
4107 data = {
4109 'comment_id': comment.comment_id,
4108 'comment_id': comment.comment_id,
4110 'comment_type': comment.comment_type,
4109 'comment_type': comment.comment_type,
4111 'comment_text': comment.text,
4110 'comment_text': comment.text,
4112 'comment_status': comment.status_change,
4111 'comment_status': comment.status_change,
4113 'comment_f_path': comment.f_path,
4112 'comment_f_path': comment.f_path,
4114 'comment_lineno': comment.line_no,
4113 'comment_lineno': comment.line_no,
4115 'comment_author': comment.author,
4114 'comment_author': comment.author,
4116 'comment_created_on': comment.created_on,
4115 'comment_created_on': comment.created_on,
4117 'comment_resolved_by': self.resolved,
4116 'comment_resolved_by': self.resolved,
4118 'comment_commit_id': comment.revision,
4117 'comment_commit_id': comment.revision,
4119 'comment_pull_request_id': comment.pull_request_id,
4118 'comment_pull_request_id': comment.pull_request_id,
4120 'comment_last_version': self.last_version
4119 'comment_last_version': self.last_version
4121 }
4120 }
4122 return data
4121 return data
4123
4122
4124 def __json__(self):
4123 def __json__(self):
4125 data = dict()
4124 data = dict()
4126 data.update(self.get_api_data())
4125 data.update(self.get_api_data())
4127 return data
4126 return data
4128
4127
4129
4128
4130 class ChangesetCommentHistory(Base, BaseModel):
4129 class ChangesetCommentHistory(Base, BaseModel):
4131 __tablename__ = 'changeset_comments_history'
4130 __tablename__ = 'changeset_comments_history'
4132 __table_args__ = (
4131 __table_args__ = (
4133 Index('cch_comment_id_idx', 'comment_id'),
4132 Index('cch_comment_id_idx', 'comment_id'),
4134 base_table_args,
4133 base_table_args,
4135 )
4134 )
4136
4135
4137 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
4136 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)
4137 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
4139 version = Column("version", Integer(), nullable=False, default=0)
4138 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)
4139 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)
4140 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)
4141 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4143 deleted = Column('deleted', Boolean(), default=False)
4142 deleted = Column('deleted', Boolean(), default=False)
4144
4143
4145 author = relationship('User', lazy='joined')
4144 author = relationship('User', lazy='joined')
4146 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4145 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
4147
4146
4148 @classmethod
4147 @classmethod
4149 def get_version(cls, comment_id):
4148 def get_version(cls, comment_id):
4150 q = Session().query(ChangesetCommentHistory).filter(
4149 q = Session().query(ChangesetCommentHistory).filter(
4151 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4150 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4152 if q.count() == 0:
4151 if q.count() == 0:
4153 return 1
4152 return 1
4154 elif q.count() >= q[0].version:
4153 elif q.count() >= q[0].version:
4155 return q.count() + 1
4154 return q.count() + 1
4156 else:
4155 else:
4157 return q[0].version + 1
4156 return q[0].version + 1
4158
4157
4159
4158
4160 class ChangesetStatus(Base, BaseModel):
4159 class ChangesetStatus(Base, BaseModel):
4161 __tablename__ = 'changeset_statuses'
4160 __tablename__ = 'changeset_statuses'
4162 __table_args__ = (
4161 __table_args__ = (
4163 Index('cs_revision_idx', 'revision'),
4162 Index('cs_revision_idx', 'revision'),
4164 Index('cs_version_idx', 'version'),
4163 Index('cs_version_idx', 'version'),
4165 UniqueConstraint('repo_id', 'revision', 'version'),
4164 UniqueConstraint('repo_id', 'revision', 'version'),
4166 base_table_args
4165 base_table_args
4167 )
4166 )
4168
4167
4169 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4168 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4170 STATUS_APPROVED = 'approved'
4169 STATUS_APPROVED = 'approved'
4171 STATUS_REJECTED = 'rejected'
4170 STATUS_REJECTED = 'rejected'
4172 STATUS_UNDER_REVIEW = 'under_review'
4171 STATUS_UNDER_REVIEW = 'under_review'
4173
4172
4174 STATUSES = [
4173 STATUSES = [
4175 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4174 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4176 (STATUS_APPROVED, _("Approved")),
4175 (STATUS_APPROVED, _("Approved")),
4177 (STATUS_REJECTED, _("Rejected")),
4176 (STATUS_REJECTED, _("Rejected")),
4178 (STATUS_UNDER_REVIEW, _("Under Review")),
4177 (STATUS_UNDER_REVIEW, _("Under Review")),
4179 ]
4178 ]
4180
4179
4181 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4180 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)
4181 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)
4182 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4184 revision = Column('revision', String(40), nullable=False)
4183 revision = Column('revision', String(40), nullable=False)
4185 status = Column('status', String(128), nullable=False, default=DEFAULT)
4184 status = Column('status', String(128), nullable=False, default=DEFAULT)
4186 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4185 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)
4186 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4188 version = Column('version', Integer(), nullable=False, default=0)
4187 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)
4188 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4190
4189
4191 author = relationship('User', lazy='select')
4190 author = relationship('User', lazy='select')
4192 repo = relationship('Repository', lazy='select')
4191 repo = relationship('Repository', lazy='select')
4193 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4192 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4194 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4193 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4195
4194
4196 def __repr__(self):
4195 def __repr__(self):
4197 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4196 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4198
4197
4199 @classmethod
4198 @classmethod
4200 def get_status_lbl(cls, value):
4199 def get_status_lbl(cls, value):
4201 return dict(cls.STATUSES).get(value)
4200 return dict(cls.STATUSES).get(value)
4202
4201
4203 @property
4202 @property
4204 def status_lbl(self):
4203 def status_lbl(self):
4205 return ChangesetStatus.get_status_lbl(self.status)
4204 return ChangesetStatus.get_status_lbl(self.status)
4206
4205
4207 def get_api_data(self):
4206 def get_api_data(self):
4208 status = self
4207 status = self
4209 data = {
4208 data = {
4210 'status_id': status.changeset_status_id,
4209 'status_id': status.changeset_status_id,
4211 'status': status.status,
4210 'status': status.status,
4212 }
4211 }
4213 return data
4212 return data
4214
4213
4215 def __json__(self):
4214 def __json__(self):
4216 data = dict()
4215 data = dict()
4217 data.update(self.get_api_data())
4216 data.update(self.get_api_data())
4218 return data
4217 return data
4219
4218
4220
4219
4221 class _SetState(object):
4220 class _SetState(object):
4222 """
4221 """
4223 Context processor allowing changing state for sensitive operation such as
4222 Context processor allowing changing state for sensitive operation such as
4224 pull request update or merge
4223 pull request update or merge
4225 """
4224 """
4226
4225
4227 def __init__(self, pull_request, pr_state, back_state=None):
4226 def __init__(self, pull_request, pr_state, back_state=None):
4228 self._pr = pull_request
4227 self._pr = pull_request
4229 self._org_state = back_state or pull_request.pull_request_state
4228 self._org_state = back_state or pull_request.pull_request_state
4230 self._pr_state = pr_state
4229 self._pr_state = pr_state
4231 self._current_state = None
4230 self._current_state = None
4232
4231
4233 def __enter__(self):
4232 def __enter__(self):
4234 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4233 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4235 self._pr, self._pr_state)
4234 self._pr, self._pr_state)
4236 self.set_pr_state(self._pr_state)
4235 self.set_pr_state(self._pr_state)
4237 return self
4236 return self
4238
4237
4239 def __exit__(self, exc_type, exc_val, exc_tb):
4238 def __exit__(self, exc_type, exc_val, exc_tb):
4240 if exc_val is not None or exc_type is not None:
4239 if exc_val is not None or exc_type is not None:
4241 log.error(traceback.format_tb(exc_tb))
4240 log.error(traceback.format_tb(exc_tb))
4242 return None
4241 return None
4243
4242
4244 self.set_pr_state(self._org_state)
4243 self.set_pr_state(self._org_state)
4245 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4244 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4246 self._pr, self._org_state)
4245 self._pr, self._org_state)
4247
4246
4248 @property
4247 @property
4249 def state(self):
4248 def state(self):
4250 return self._current_state
4249 return self._current_state
4251
4250
4252 def set_pr_state(self, pr_state):
4251 def set_pr_state(self, pr_state):
4253 try:
4252 try:
4254 self._pr.pull_request_state = pr_state
4253 self._pr.pull_request_state = pr_state
4255 Session().add(self._pr)
4254 Session().add(self._pr)
4256 Session().commit()
4255 Session().commit()
4257 self._current_state = pr_state
4256 self._current_state = pr_state
4258 except Exception:
4257 except Exception:
4259 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4258 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4260 raise
4259 raise
4261
4260
4262
4261
4263 class _PullRequestBase(BaseModel):
4262 class _PullRequestBase(BaseModel):
4264 """
4263 """
4265 Common attributes of pull request and version entries.
4264 Common attributes of pull request and version entries.
4266 """
4265 """
4267
4266
4268 # .status values
4267 # .status values
4269 STATUS_NEW = 'new'
4268 STATUS_NEW = 'new'
4270 STATUS_OPEN = 'open'
4269 STATUS_OPEN = 'open'
4271 STATUS_CLOSED = 'closed'
4270 STATUS_CLOSED = 'closed'
4272
4271
4273 # available states
4272 # available states
4274 STATE_CREATING = 'creating'
4273 STATE_CREATING = 'creating'
4275 STATE_UPDATING = 'updating'
4274 STATE_UPDATING = 'updating'
4276 STATE_MERGING = 'merging'
4275 STATE_MERGING = 'merging'
4277 STATE_CREATED = 'created'
4276 STATE_CREATED = 'created'
4278
4277
4279 title = Column('title', Unicode(255), nullable=True)
4278 title = Column('title', Unicode(255), nullable=True)
4280 description = Column(
4279 description = Column(
4281 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4280 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4282 nullable=True)
4281 nullable=True)
4283 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4282 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4284
4283
4285 # new/open/closed status of pull request (not approve/reject/etc)
4284 # new/open/closed status of pull request (not approve/reject/etc)
4286 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4285 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4287 created_on = Column(
4286 created_on = Column(
4288 'created_on', DateTime(timezone=False), nullable=False,
4287 'created_on', DateTime(timezone=False), nullable=False,
4289 default=datetime.datetime.now)
4288 default=datetime.datetime.now)
4290 updated_on = Column(
4289 updated_on = Column(
4291 'updated_on', DateTime(timezone=False), nullable=False,
4290 'updated_on', DateTime(timezone=False), nullable=False,
4292 default=datetime.datetime.now)
4291 default=datetime.datetime.now)
4293
4292
4294 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4293 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4295
4294
4296 @declared_attr
4295 @declared_attr
4297 def user_id(cls):
4296 def user_id(cls):
4298 return Column(
4297 return Column(
4299 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4298 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4300 unique=None)
4299 unique=None)
4301
4300
4302 # 500 revisions max
4301 # 500 revisions max
4303 _revisions = Column(
4302 _revisions = Column(
4304 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4303 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4305
4304
4306 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4305 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4307
4306
4308 @declared_attr
4307 @declared_attr
4309 def source_repo_id(cls):
4308 def source_repo_id(cls):
4310 # TODO: dan: rename column to source_repo_id
4309 # TODO: dan: rename column to source_repo_id
4311 return Column(
4310 return Column(
4312 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4311 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4313 nullable=False)
4312 nullable=False)
4314
4313
4315 @declared_attr
4314 @declared_attr
4316 def pr_source(cls):
4315 def pr_source(cls):
4317 return relationship(
4316 return relationship(
4318 'Repository',
4317 'Repository',
4319 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4318 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4320 overlaps="pull_requests_source"
4319 overlaps="pull_requests_source"
4321 )
4320 )
4322
4321
4323 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4322 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4324
4323
4325 @hybrid_property
4324 @hybrid_property
4326 def source_ref(self):
4325 def source_ref(self):
4327 return self._source_ref
4326 return self._source_ref
4328
4327
4329 @source_ref.setter
4328 @source_ref.setter
4330 def source_ref(self, val):
4329 def source_ref(self, val):
4331 parts = (val or '').split(':')
4330 parts = (val or '').split(':')
4332 if len(parts) != 3:
4331 if len(parts) != 3:
4333 raise ValueError(
4332 raise ValueError(
4334 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4333 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4335 self._source_ref = safe_str(val)
4334 self._source_ref = safe_str(val)
4336
4335
4337 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4336 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4338
4337
4339 @hybrid_property
4338 @hybrid_property
4340 def target_ref(self):
4339 def target_ref(self):
4341 return self._target_ref
4340 return self._target_ref
4342
4341
4343 @target_ref.setter
4342 @target_ref.setter
4344 def target_ref(self, val):
4343 def target_ref(self, val):
4345 parts = (val or '').split(':')
4344 parts = (val or '').split(':')
4346 if len(parts) != 3:
4345 if len(parts) != 3:
4347 raise ValueError(
4346 raise ValueError(
4348 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4347 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4349 self._target_ref = safe_str(val)
4348 self._target_ref = safe_str(val)
4350
4349
4351 @declared_attr
4350 @declared_attr
4352 def target_repo_id(cls):
4351 def target_repo_id(cls):
4353 # TODO: dan: rename column to target_repo_id
4352 # TODO: dan: rename column to target_repo_id
4354 return Column(
4353 return Column(
4355 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4354 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4356 nullable=False)
4355 nullable=False)
4357
4356
4358 @declared_attr
4357 @declared_attr
4359 def pr_target(cls):
4358 def pr_target(cls):
4360 return relationship(
4359 return relationship(
4361 'Repository',
4360 'Repository',
4362 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4361 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4363 overlaps="pull_requests_target"
4362 overlaps="pull_requests_target"
4364 )
4363 )
4365
4364
4366 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4365 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4367
4366
4368 # TODO: dan: rename column to last_merge_source_rev
4367 # TODO: dan: rename column to last_merge_source_rev
4369 _last_merge_source_rev = Column(
4368 _last_merge_source_rev = Column(
4370 'last_merge_org_rev', String(40), nullable=True)
4369 'last_merge_org_rev', String(40), nullable=True)
4371 # TODO: dan: rename column to last_merge_target_rev
4370 # TODO: dan: rename column to last_merge_target_rev
4372 _last_merge_target_rev = Column(
4371 _last_merge_target_rev = Column(
4373 'last_merge_other_rev', String(40), nullable=True)
4372 'last_merge_other_rev', String(40), nullable=True)
4374 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4373 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4375 last_merge_metadata = Column(
4374 last_merge_metadata = Column(
4376 'last_merge_metadata', MutationObj.as_mutable(
4375 'last_merge_metadata', MutationObj.as_mutable(
4377 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4376 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4378
4377
4379 merge_rev = Column('merge_rev', String(40), nullable=True)
4378 merge_rev = Column('merge_rev', String(40), nullable=True)
4380
4379
4381 reviewer_data = Column(
4380 reviewer_data = Column(
4382 'reviewer_data_json', MutationObj.as_mutable(
4381 'reviewer_data_json', MutationObj.as_mutable(
4383 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4382 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4384
4383
4385 @property
4384 @property
4386 def reviewer_data_json(self):
4385 def reviewer_data_json(self):
4387 return str_json(self.reviewer_data)
4386 return str_json(self.reviewer_data)
4388
4387
4389 @property
4388 @property
4390 def last_merge_metadata_parsed(self):
4389 def last_merge_metadata_parsed(self):
4391 metadata = {}
4390 metadata = {}
4392 if not self.last_merge_metadata:
4391 if not self.last_merge_metadata:
4393 return metadata
4392 return metadata
4394
4393
4395 if hasattr(self.last_merge_metadata, 'de_coerce'):
4394 if hasattr(self.last_merge_metadata, 'de_coerce'):
4396 for k, v in self.last_merge_metadata.de_coerce().items():
4395 for k, v in self.last_merge_metadata.de_coerce().items():
4397 if k in ['target_ref', 'source_ref']:
4396 if k in ['target_ref', 'source_ref']:
4398 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4397 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4399 else:
4398 else:
4400 if hasattr(v, 'de_coerce'):
4399 if hasattr(v, 'de_coerce'):
4401 metadata[k] = v.de_coerce()
4400 metadata[k] = v.de_coerce()
4402 else:
4401 else:
4403 metadata[k] = v
4402 metadata[k] = v
4404 return metadata
4403 return metadata
4405
4404
4406 @property
4405 @property
4407 def work_in_progress(self):
4406 def work_in_progress(self):
4408 """checks if pull request is work in progress by checking the title"""
4407 """checks if pull request is work in progress by checking the title"""
4409 title = self.title.upper()
4408 title = self.title.upper()
4410 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4409 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4411 return True
4410 return True
4412 return False
4411 return False
4413
4412
4414 @property
4413 @property
4415 def title_safe(self):
4414 def title_safe(self):
4416 return self.title\
4415 return self.title\
4417 .replace('{', '{{')\
4416 .replace('{', '{{')\
4418 .replace('}', '}}')
4417 .replace('}', '}}')
4419
4418
4420 @hybrid_property
4419 @hybrid_property
4421 def description_safe(self):
4420 def description_safe(self):
4422 from rhodecode.lib import helpers as h
4421 from rhodecode.lib import helpers as h
4423 return h.escape(self.description)
4422 return h.escape(self.description)
4424
4423
4425 @hybrid_property
4424 @hybrid_property
4426 def revisions(self):
4425 def revisions(self):
4427 return self._revisions.split(':') if self._revisions else []
4426 return self._revisions.split(':') if self._revisions else []
4428
4427
4429 @revisions.setter
4428 @revisions.setter
4430 def revisions(self, val):
4429 def revisions(self, val):
4431 self._revisions = ':'.join(val)
4430 self._revisions = ':'.join(val)
4432
4431
4433 @hybrid_property
4432 @hybrid_property
4434 def last_merge_status(self):
4433 def last_merge_status(self):
4435 return safe_int(self._last_merge_status)
4434 return safe_int(self._last_merge_status)
4436
4435
4437 @last_merge_status.setter
4436 @last_merge_status.setter
4438 def last_merge_status(self, val):
4437 def last_merge_status(self, val):
4439 self._last_merge_status = val
4438 self._last_merge_status = val
4440
4439
4441 @declared_attr
4440 @declared_attr
4442 def author(cls):
4441 def author(cls):
4443 return relationship(
4442 return relationship(
4444 'User', lazy='joined',
4443 'User', lazy='joined',
4445 #TODO, problem that is somehow :?
4444 #TODO, problem that is somehow :?
4446 #back_populates='user_pull_requests'
4445 #back_populates='user_pull_requests'
4447 )
4446 )
4448
4447
4449 @declared_attr
4448 @declared_attr
4450 def source_repo(cls):
4449 def source_repo(cls):
4451 return relationship(
4450 return relationship(
4452 'Repository',
4451 'Repository',
4453 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4452 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4454 overlaps="pr_source"
4453 overlaps="pr_source"
4455 )
4454 )
4456
4455
4457 @property
4456 @property
4458 def source_ref_parts(self):
4457 def source_ref_parts(self):
4459 return self.unicode_to_reference(self.source_ref)
4458 return self.unicode_to_reference(self.source_ref)
4460
4459
4461 @declared_attr
4460 @declared_attr
4462 def target_repo(cls):
4461 def target_repo(cls):
4463 return relationship(
4462 return relationship(
4464 'Repository',
4463 'Repository',
4465 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4464 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4466 overlaps="pr_target"
4465 overlaps="pr_target"
4467 )
4466 )
4468
4467
4469 @property
4468 @property
4470 def target_ref_parts(self):
4469 def target_ref_parts(self):
4471 return self.unicode_to_reference(self.target_ref)
4470 return self.unicode_to_reference(self.target_ref)
4472
4471
4473 @property
4472 @property
4474 def shadow_merge_ref(self):
4473 def shadow_merge_ref(self):
4475 return self.unicode_to_reference(self._shadow_merge_ref)
4474 return self.unicode_to_reference(self._shadow_merge_ref)
4476
4475
4477 @shadow_merge_ref.setter
4476 @shadow_merge_ref.setter
4478 def shadow_merge_ref(self, ref):
4477 def shadow_merge_ref(self, ref):
4479 self._shadow_merge_ref = self.reference_to_unicode(ref)
4478 self._shadow_merge_ref = self.reference_to_unicode(ref)
4480
4479
4481 @staticmethod
4480 @staticmethod
4482 def unicode_to_reference(raw):
4481 def unicode_to_reference(raw):
4483 return unicode_to_reference(raw)
4482 return unicode_to_reference(raw)
4484
4483
4485 @staticmethod
4484 @staticmethod
4486 def reference_to_unicode(ref):
4485 def reference_to_unicode(ref):
4487 return reference_to_unicode(ref)
4486 return reference_to_unicode(ref)
4488
4487
4489 def get_api_data(self, with_merge_state=True):
4488 def get_api_data(self, with_merge_state=True):
4490 from rhodecode.model.pull_request import PullRequestModel
4489 from rhodecode.model.pull_request import PullRequestModel
4491
4490
4492 pull_request = self
4491 pull_request = self
4493 if with_merge_state:
4492 if with_merge_state:
4494 merge_response, merge_status, msg = \
4493 merge_response, merge_status, msg = \
4495 PullRequestModel().merge_status(pull_request)
4494 PullRequestModel().merge_status(pull_request)
4496 merge_state = {
4495 merge_state = {
4497 'status': merge_status,
4496 'status': merge_status,
4498 'message': safe_str(msg),
4497 'message': safe_str(msg),
4499 }
4498 }
4500 else:
4499 else:
4501 merge_state = {'status': 'not_available',
4500 merge_state = {'status': 'not_available',
4502 'message': 'not_available'}
4501 'message': 'not_available'}
4503
4502
4504 merge_data = {
4503 merge_data = {
4505 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4504 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4506 'reference': (
4505 'reference': (
4507 pull_request.shadow_merge_ref.asdict()
4506 pull_request.shadow_merge_ref.asdict()
4508 if pull_request.shadow_merge_ref else None),
4507 if pull_request.shadow_merge_ref else None),
4509 }
4508 }
4510
4509
4511 data = {
4510 data = {
4512 'pull_request_id': pull_request.pull_request_id,
4511 'pull_request_id': pull_request.pull_request_id,
4513 'url': PullRequestModel().get_url(pull_request),
4512 'url': PullRequestModel().get_url(pull_request),
4514 'title': pull_request.title,
4513 'title': pull_request.title,
4515 'description': pull_request.description,
4514 'description': pull_request.description,
4516 'status': pull_request.status,
4515 'status': pull_request.status,
4517 'state': pull_request.pull_request_state,
4516 'state': pull_request.pull_request_state,
4518 'created_on': pull_request.created_on,
4517 'created_on': pull_request.created_on,
4519 'updated_on': pull_request.updated_on,
4518 'updated_on': pull_request.updated_on,
4520 'commit_ids': pull_request.revisions,
4519 'commit_ids': pull_request.revisions,
4521 'review_status': pull_request.calculated_review_status(),
4520 'review_status': pull_request.calculated_review_status(),
4522 'mergeable': merge_state,
4521 'mergeable': merge_state,
4523 'source': {
4522 'source': {
4524 'clone_url': pull_request.source_repo.clone_url(),
4523 'clone_url': pull_request.source_repo.clone_url(),
4525 'repository': pull_request.source_repo.repo_name,
4524 'repository': pull_request.source_repo.repo_name,
4526 'reference': {
4525 'reference': {
4527 'name': pull_request.source_ref_parts.name,
4526 'name': pull_request.source_ref_parts.name,
4528 'type': pull_request.source_ref_parts.type,
4527 'type': pull_request.source_ref_parts.type,
4529 'commit_id': pull_request.source_ref_parts.commit_id,
4528 'commit_id': pull_request.source_ref_parts.commit_id,
4530 },
4529 },
4531 },
4530 },
4532 'target': {
4531 'target': {
4533 'clone_url': pull_request.target_repo.clone_url(),
4532 'clone_url': pull_request.target_repo.clone_url(),
4534 'repository': pull_request.target_repo.repo_name,
4533 'repository': pull_request.target_repo.repo_name,
4535 'reference': {
4534 'reference': {
4536 'name': pull_request.target_ref_parts.name,
4535 'name': pull_request.target_ref_parts.name,
4537 'type': pull_request.target_ref_parts.type,
4536 'type': pull_request.target_ref_parts.type,
4538 'commit_id': pull_request.target_ref_parts.commit_id,
4537 'commit_id': pull_request.target_ref_parts.commit_id,
4539 },
4538 },
4540 },
4539 },
4541 'merge': merge_data,
4540 'merge': merge_data,
4542 'author': pull_request.author.get_api_data(include_secrets=False,
4541 'author': pull_request.author.get_api_data(include_secrets=False,
4543 details='basic'),
4542 details='basic'),
4544 'reviewers': [
4543 'reviewers': [
4545 {
4544 {
4546 'user': reviewer.get_api_data(include_secrets=False,
4545 'user': reviewer.get_api_data(include_secrets=False,
4547 details='basic'),
4546 details='basic'),
4548 'reasons': reasons,
4547 'reasons': reasons,
4549 'review_status': st[0][1].status if st else 'not_reviewed',
4548 'review_status': st[0][1].status if st else 'not_reviewed',
4550 }
4549 }
4551 for obj, reviewer, reasons, mandatory, st in
4550 for obj, reviewer, reasons, mandatory, st in
4552 pull_request.reviewers_statuses()
4551 pull_request.reviewers_statuses()
4553 ]
4552 ]
4554 }
4553 }
4555
4554
4556 return data
4555 return data
4557
4556
4558 def set_state(self, pull_request_state, final_state=None):
4557 def set_state(self, pull_request_state, final_state=None):
4559 """
4558 """
4560 # goes from initial state to updating to initial state.
4559 # goes from initial state to updating to initial state.
4561 # initial state can be changed by specifying back_state=
4560 # initial state can be changed by specifying back_state=
4562 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4561 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4563 pull_request.merge()
4562 pull_request.merge()
4564
4563
4565 :param pull_request_state:
4564 :param pull_request_state:
4566 :param final_state:
4565 :param final_state:
4567
4566
4568 """
4567 """
4569
4568
4570 return _SetState(self, pull_request_state, back_state=final_state)
4569 return _SetState(self, pull_request_state, back_state=final_state)
4571
4570
4572
4571
4573 class PullRequest(Base, _PullRequestBase):
4572 class PullRequest(Base, _PullRequestBase):
4574 __tablename__ = 'pull_requests'
4573 __tablename__ = 'pull_requests'
4575 __table_args__ = (
4574 __table_args__ = (
4576 base_table_args,
4575 base_table_args,
4577 )
4576 )
4578 LATEST_VER = 'latest'
4577 LATEST_VER = 'latest'
4579
4578
4580 pull_request_id = Column(
4579 pull_request_id = Column(
4581 'pull_request_id', Integer(), nullable=False, primary_key=True)
4580 'pull_request_id', Integer(), nullable=False, primary_key=True)
4582
4581
4583 def __repr__(self):
4582 def __repr__(self):
4584 if self.pull_request_id:
4583 if self.pull_request_id:
4585 return f'<DB:PullRequest #{self.pull_request_id}>'
4584 return f'<DB:PullRequest #{self.pull_request_id}>'
4586 else:
4585 else:
4587 return f'<DB:PullRequest at {id(self)!r}>'
4586 return f'<DB:PullRequest at {id(self)!r}>'
4588
4587
4589 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4588 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4590 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4589 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4591 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4590 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')
4591 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4593
4592
4594 @classmethod
4593 @classmethod
4595 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4594 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4596 internal_methods=None):
4595 internal_methods=None):
4597
4596
4598 class PullRequestDisplay(object):
4597 class PullRequestDisplay(object):
4599 """
4598 """
4600 Special object wrapper for showing PullRequest data via Versions
4599 Special object wrapper for showing PullRequest data via Versions
4601 It mimics PR object as close as possible. This is read only object
4600 It mimics PR object as close as possible. This is read only object
4602 just for display
4601 just for display
4603 """
4602 """
4604
4603
4605 def __init__(self, attrs, internal=None):
4604 def __init__(self, attrs, internal=None):
4606 self.attrs = attrs
4605 self.attrs = attrs
4607 # internal have priority over the given ones via attrs
4606 # internal have priority over the given ones via attrs
4608 self.internal = internal or ['versions']
4607 self.internal = internal or ['versions']
4609
4608
4610 def __getattr__(self, item):
4609 def __getattr__(self, item):
4611 if item in self.internal:
4610 if item in self.internal:
4612 return getattr(self, item)
4611 return getattr(self, item)
4613 try:
4612 try:
4614 return self.attrs[item]
4613 return self.attrs[item]
4615 except KeyError:
4614 except KeyError:
4616 raise AttributeError(
4615 raise AttributeError(
4617 '%s object has no attribute %s' % (self, item))
4616 '%s object has no attribute %s' % (self, item))
4618
4617
4619 def __repr__(self):
4618 def __repr__(self):
4620 pr_id = self.attrs.get('pull_request_id')
4619 pr_id = self.attrs.get('pull_request_id')
4621 return f'<DB:PullRequestDisplay #{pr_id}>'
4620 return f'<DB:PullRequestDisplay #{pr_id}>'
4622
4621
4623 def versions(self):
4622 def versions(self):
4624 return pull_request_obj.versions.order_by(
4623 return pull_request_obj.versions.order_by(
4625 PullRequestVersion.pull_request_version_id).all()
4624 PullRequestVersion.pull_request_version_id).all()
4626
4625
4627 def is_closed(self):
4626 def is_closed(self):
4628 return pull_request_obj.is_closed()
4627 return pull_request_obj.is_closed()
4629
4628
4630 def is_state_changing(self):
4629 def is_state_changing(self):
4631 return pull_request_obj.is_state_changing()
4630 return pull_request_obj.is_state_changing()
4632
4631
4633 @property
4632 @property
4634 def pull_request_version_id(self):
4633 def pull_request_version_id(self):
4635 return getattr(pull_request_obj, 'pull_request_version_id', None)
4634 return getattr(pull_request_obj, 'pull_request_version_id', None)
4636
4635
4637 @property
4636 @property
4638 def pull_request_last_version(self):
4637 def pull_request_last_version(self):
4639 return pull_request_obj.pull_request_last_version
4638 return pull_request_obj.pull_request_last_version
4640
4639
4641 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4640 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4642
4641
4643 attrs.author = StrictAttributeDict(
4642 attrs.author = StrictAttributeDict(
4644 pull_request_obj.author.get_api_data())
4643 pull_request_obj.author.get_api_data())
4645 if pull_request_obj.target_repo:
4644 if pull_request_obj.target_repo:
4646 attrs.target_repo = StrictAttributeDict(
4645 attrs.target_repo = StrictAttributeDict(
4647 pull_request_obj.target_repo.get_api_data())
4646 pull_request_obj.target_repo.get_api_data())
4648 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4647 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4649
4648
4650 if pull_request_obj.source_repo:
4649 if pull_request_obj.source_repo:
4651 attrs.source_repo = StrictAttributeDict(
4650 attrs.source_repo = StrictAttributeDict(
4652 pull_request_obj.source_repo.get_api_data())
4651 pull_request_obj.source_repo.get_api_data())
4653 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4652 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4654
4653
4655 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4654 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4656 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4655 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4657 attrs.revisions = pull_request_obj.revisions
4656 attrs.revisions = pull_request_obj.revisions
4658 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4657 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4659 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4658 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4660 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4659 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4661 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4660 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4662
4661
4663 return PullRequestDisplay(attrs, internal=internal_methods)
4662 return PullRequestDisplay(attrs, internal=internal_methods)
4664
4663
4665 def is_closed(self):
4664 def is_closed(self):
4666 return self.status == self.STATUS_CLOSED
4665 return self.status == self.STATUS_CLOSED
4667
4666
4668 def is_state_changing(self):
4667 def is_state_changing(self):
4669 return self.pull_request_state != PullRequest.STATE_CREATED
4668 return self.pull_request_state != PullRequest.STATE_CREATED
4670
4669
4671 def __json__(self):
4670 def __json__(self):
4672 return {
4671 return {
4673 'revisions': self.revisions,
4672 'revisions': self.revisions,
4674 'versions': self.versions_count
4673 'versions': self.versions_count
4675 }
4674 }
4676
4675
4677 def calculated_review_status(self):
4676 def calculated_review_status(self):
4678 from rhodecode.model.changeset_status import ChangesetStatusModel
4677 from rhodecode.model.changeset_status import ChangesetStatusModel
4679 return ChangesetStatusModel().calculated_review_status(self)
4678 return ChangesetStatusModel().calculated_review_status(self)
4680
4679
4681 def reviewers_statuses(self, user=None):
4680 def reviewers_statuses(self, user=None):
4682 from rhodecode.model.changeset_status import ChangesetStatusModel
4681 from rhodecode.model.changeset_status import ChangesetStatusModel
4683 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4682 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4684
4683
4685 def get_pull_request_reviewers(self, role=None):
4684 def get_pull_request_reviewers(self, role=None):
4686 qry = PullRequestReviewers.query()\
4685 qry = PullRequestReviewers.query()\
4687 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4686 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4688 if role:
4687 if role:
4689 qry = qry.filter(PullRequestReviewers.role == role)
4688 qry = qry.filter(PullRequestReviewers.role == role)
4690
4689
4691 return qry.all()
4690 return qry.all()
4692
4691
4693 @property
4692 @property
4694 def reviewers_count(self):
4693 def reviewers_count(self):
4695 qry = PullRequestReviewers.query()\
4694 qry = PullRequestReviewers.query()\
4696 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4695 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4697 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4696 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4698 return qry.count()
4697 return qry.count()
4699
4698
4700 @property
4699 @property
4701 def observers_count(self):
4700 def observers_count(self):
4702 qry = PullRequestReviewers.query()\
4701 qry = PullRequestReviewers.query()\
4703 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4702 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4704 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4703 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4705 return qry.count()
4704 return qry.count()
4706
4705
4707 def observers(self):
4706 def observers(self):
4708 qry = PullRequestReviewers.query()\
4707 qry = PullRequestReviewers.query()\
4709 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4708 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4710 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4709 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4711 .all()
4710 .all()
4712
4711
4713 for entry in qry:
4712 for entry in qry:
4714 yield entry, entry.user
4713 yield entry, entry.user
4715
4714
4716 @property
4715 @property
4717 def workspace_id(self):
4716 def workspace_id(self):
4718 from rhodecode.model.pull_request import PullRequestModel
4717 from rhodecode.model.pull_request import PullRequestModel
4719 return PullRequestModel()._workspace_id(self)
4718 return PullRequestModel()._workspace_id(self)
4720
4719
4721 def get_shadow_repo(self):
4720 def get_shadow_repo(self):
4722 workspace_id = self.workspace_id
4721 workspace_id = self.workspace_id
4723 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4722 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4724 if os.path.isdir(shadow_repository_path):
4723 if os.path.isdir(shadow_repository_path):
4725 vcs_obj = self.target_repo.scm_instance()
4724 vcs_obj = self.target_repo.scm_instance()
4726 return vcs_obj.get_shadow_instance(shadow_repository_path)
4725 return vcs_obj.get_shadow_instance(shadow_repository_path)
4727
4726
4728 @property
4727 @property
4729 def versions_count(self):
4728 def versions_count(self):
4730 """
4729 """
4731 return number of versions this PR have, e.g a PR that once been
4730 return number of versions this PR have, e.g a PR that once been
4732 updated will have 2 versions
4731 updated will have 2 versions
4733 """
4732 """
4734 return self.versions.count() + 1
4733 return self.versions.count() + 1
4735
4734
4736 @property
4735 @property
4737 def pull_request_last_version(self):
4736 def pull_request_last_version(self):
4738 return self.versions_count
4737 return self.versions_count
4739
4738
4740
4739
4741 class PullRequestVersion(Base, _PullRequestBase):
4740 class PullRequestVersion(Base, _PullRequestBase):
4742 __tablename__ = 'pull_request_versions'
4741 __tablename__ = 'pull_request_versions'
4743 __table_args__ = (
4742 __table_args__ = (
4744 base_table_args,
4743 base_table_args,
4745 )
4744 )
4746
4745
4747 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4746 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)
4747 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4749 pull_request = relationship('PullRequest', back_populates='versions')
4748 pull_request = relationship('PullRequest', back_populates='versions')
4750
4749
4751 def __repr__(self):
4750 def __repr__(self):
4752 if self.pull_request_version_id:
4751 if self.pull_request_version_id:
4753 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4752 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4754 else:
4753 else:
4755 return f'<DB:PullRequestVersion at {id(self)!r}>'
4754 return f'<DB:PullRequestVersion at {id(self)!r}>'
4756
4755
4757 @property
4756 @property
4758 def reviewers(self):
4757 def reviewers(self):
4759 return self.pull_request.reviewers
4758 return self.pull_request.reviewers
4760
4759
4761 @property
4760 @property
4762 def versions(self):
4761 def versions(self):
4763 return self.pull_request.versions
4762 return self.pull_request.versions
4764
4763
4765 def is_closed(self):
4764 def is_closed(self):
4766 # calculate from original
4765 # calculate from original
4767 return self.pull_request.status == self.STATUS_CLOSED
4766 return self.pull_request.status == self.STATUS_CLOSED
4768
4767
4769 def is_state_changing(self):
4768 def is_state_changing(self):
4770 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4769 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4771
4770
4772 def calculated_review_status(self):
4771 def calculated_review_status(self):
4773 return self.pull_request.calculated_review_status()
4772 return self.pull_request.calculated_review_status()
4774
4773
4775 def reviewers_statuses(self):
4774 def reviewers_statuses(self):
4776 return self.pull_request.reviewers_statuses()
4775 return self.pull_request.reviewers_statuses()
4777
4776
4778 def observers(self):
4777 def observers(self):
4779 return self.pull_request.observers()
4778 return self.pull_request.observers()
4780
4779
4781
4780
4782 class PullRequestReviewers(Base, BaseModel):
4781 class PullRequestReviewers(Base, BaseModel):
4783 __tablename__ = 'pull_request_reviewers'
4782 __tablename__ = 'pull_request_reviewers'
4784 __table_args__ = (
4783 __table_args__ = (
4785 base_table_args,
4784 base_table_args,
4786 )
4785 )
4787 ROLE_REVIEWER = 'reviewer'
4786 ROLE_REVIEWER = 'reviewer'
4788 ROLE_OBSERVER = 'observer'
4787 ROLE_OBSERVER = 'observer'
4789 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4788 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4790
4789
4791 @hybrid_property
4790 @hybrid_property
4792 def reasons(self):
4791 def reasons(self):
4793 if not self._reasons:
4792 if not self._reasons:
4794 return []
4793 return []
4795 return self._reasons
4794 return self._reasons
4796
4795
4797 @reasons.setter
4796 @reasons.setter
4798 def reasons(self, val):
4797 def reasons(self, val):
4799 val = val or []
4798 val = val or []
4800 if any(not isinstance(x, str) for x in val):
4799 if any(not isinstance(x, str) for x in val):
4801 raise Exception('invalid reasons type, must be list of strings')
4800 raise Exception('invalid reasons type, must be list of strings')
4802 self._reasons = val
4801 self._reasons = val
4803
4802
4804 pull_requests_reviewers_id = Column(
4803 pull_requests_reviewers_id = Column(
4805 'pull_requests_reviewers_id', Integer(), nullable=False,
4804 'pull_requests_reviewers_id', Integer(), nullable=False,
4806 primary_key=True)
4805 primary_key=True)
4807 pull_request_id = Column(
4806 pull_request_id = Column(
4808 "pull_request_id", Integer(),
4807 "pull_request_id", Integer(),
4809 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4808 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4810 user_id = Column(
4809 user_id = Column(
4811 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4810 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4812 _reasons = Column(
4811 _reasons = Column(
4813 'reason', MutationList.as_mutable(
4812 'reason', MutationList.as_mutable(
4814 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4813 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4815
4814
4816 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4815 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4817 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4816 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4818
4817
4819 user = relationship('User')
4818 user = relationship('User')
4820 pull_request = relationship('PullRequest', back_populates='reviewers')
4819 pull_request = relationship('PullRequest', back_populates='reviewers')
4821
4820
4822 rule_data = Column(
4821 rule_data = Column(
4823 'rule_data_json',
4822 'rule_data_json',
4824 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4823 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4825
4824
4826 def rule_user_group_data(self):
4825 def rule_user_group_data(self):
4827 """
4826 """
4828 Returns the voting user group rule data for this reviewer
4827 Returns the voting user group rule data for this reviewer
4829 """
4828 """
4830
4829
4831 if self.rule_data and 'vote_rule' in self.rule_data:
4830 if self.rule_data and 'vote_rule' in self.rule_data:
4832 user_group_data = {}
4831 user_group_data = {}
4833 if 'rule_user_group_entry_id' in self.rule_data:
4832 if 'rule_user_group_entry_id' in self.rule_data:
4834 # means a group with voting rules !
4833 # means a group with voting rules !
4835 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4834 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4836 user_group_data['name'] = self.rule_data['rule_name']
4835 user_group_data['name'] = self.rule_data['rule_name']
4837 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4836 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4838
4837
4839 return user_group_data
4838 return user_group_data
4840
4839
4841 @classmethod
4840 @classmethod
4842 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4841 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4843 qry = PullRequestReviewers.query()\
4842 qry = PullRequestReviewers.query()\
4844 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4843 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4845 if role:
4844 if role:
4846 qry = qry.filter(PullRequestReviewers.role == role)
4845 qry = qry.filter(PullRequestReviewers.role == role)
4847
4846
4848 return qry.all()
4847 return qry.all()
4849
4848
4850 def __repr__(self):
4849 def __repr__(self):
4851 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4850 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4852
4851
4853
4852
4854 class Notification(Base, BaseModel):
4853 class Notification(Base, BaseModel):
4855 __tablename__ = 'notifications'
4854 __tablename__ = 'notifications'
4856 __table_args__ = (
4855 __table_args__ = (
4857 Index('notification_type_idx', 'type'),
4856 Index('notification_type_idx', 'type'),
4858 base_table_args,
4857 base_table_args,
4859 )
4858 )
4860
4859
4861 TYPE_CHANGESET_COMMENT = 'cs_comment'
4860 TYPE_CHANGESET_COMMENT = 'cs_comment'
4862 TYPE_MESSAGE = 'message'
4861 TYPE_MESSAGE = 'message'
4863 TYPE_MENTION = 'mention'
4862 TYPE_MENTION = 'mention'
4864 TYPE_REGISTRATION = 'registration'
4863 TYPE_REGISTRATION = 'registration'
4865 TYPE_PULL_REQUEST = 'pull_request'
4864 TYPE_PULL_REQUEST = 'pull_request'
4866 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4865 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4867 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4866 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4868
4867
4869 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4868 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4870 subject = Column('subject', Unicode(512), nullable=True)
4869 subject = Column('subject', Unicode(512), nullable=True)
4871 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4870 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4872 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4871 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)
4872 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4874 type_ = Column('type', Unicode(255))
4873 type_ = Column('type', Unicode(255))
4875
4874
4876 created_by_user = relationship('User', back_populates='user_created_notifications')
4875 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')
4876 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4878
4877
4879 @property
4878 @property
4880 def recipients(self):
4879 def recipients(self):
4881 return [x.user for x in UserNotification.query()\
4880 return [x.user for x in UserNotification.query()\
4882 .filter(UserNotification.notification == self)\
4881 .filter(UserNotification.notification == self)\
4883 .order_by(UserNotification.user_id.asc()).all()]
4882 .order_by(UserNotification.user_id.asc()).all()]
4884
4883
4885 @classmethod
4884 @classmethod
4886 def create(cls, created_by, subject, body, recipients, type_=None):
4885 def create(cls, created_by, subject, body, recipients, type_=None):
4887 if type_ is None:
4886 if type_ is None:
4888 type_ = Notification.TYPE_MESSAGE
4887 type_ = Notification.TYPE_MESSAGE
4889
4888
4890 notification = cls()
4889 notification = cls()
4891 notification.created_by_user = created_by
4890 notification.created_by_user = created_by
4892 notification.subject = subject
4891 notification.subject = subject
4893 notification.body = body
4892 notification.body = body
4894 notification.type_ = type_
4893 notification.type_ = type_
4895 notification.created_on = datetime.datetime.now()
4894 notification.created_on = datetime.datetime.now()
4896
4895
4897 # For each recipient link the created notification to his account
4896 # For each recipient link the created notification to his account
4898 for u in recipients:
4897 for u in recipients:
4899 assoc = UserNotification()
4898 assoc = UserNotification()
4900 assoc.user_id = u.user_id
4899 assoc.user_id = u.user_id
4901 assoc.notification = notification
4900 assoc.notification = notification
4902
4901
4903 # if created_by is inside recipients mark his notification
4902 # if created_by is inside recipients mark his notification
4904 # as read
4903 # as read
4905 if u.user_id == created_by.user_id:
4904 if u.user_id == created_by.user_id:
4906 assoc.read = True
4905 assoc.read = True
4907 Session().add(assoc)
4906 Session().add(assoc)
4908
4907
4909 Session().add(notification)
4908 Session().add(notification)
4910
4909
4911 return notification
4910 return notification
4912
4911
4913
4912
4914 class UserNotification(Base, BaseModel):
4913 class UserNotification(Base, BaseModel):
4915 __tablename__ = 'user_to_notification'
4914 __tablename__ = 'user_to_notification'
4916 __table_args__ = (
4915 __table_args__ = (
4917 UniqueConstraint('user_id', 'notification_id'),
4916 UniqueConstraint('user_id', 'notification_id'),
4918 base_table_args
4917 base_table_args
4919 )
4918 )
4920
4919
4921 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4920 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)
4921 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4923 read = Column('read', Boolean, default=False)
4922 read = Column('read', Boolean, default=False)
4924 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4923 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4925
4924
4926 user = relationship('User', lazy="joined", back_populates='notifications')
4925 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')
4926 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4928
4927
4929 def mark_as_read(self):
4928 def mark_as_read(self):
4930 self.read = True
4929 self.read = True
4931 Session().add(self)
4930 Session().add(self)
4932
4931
4933
4932
4934 class UserNotice(Base, BaseModel):
4933 class UserNotice(Base, BaseModel):
4935 __tablename__ = 'user_notices'
4934 __tablename__ = 'user_notices'
4936 __table_args__ = (
4935 __table_args__ = (
4937 base_table_args
4936 base_table_args
4938 )
4937 )
4939
4938
4940 NOTIFICATION_TYPE_MESSAGE = 'message'
4939 NOTIFICATION_TYPE_MESSAGE = 'message'
4941 NOTIFICATION_TYPE_NOTICE = 'notice'
4940 NOTIFICATION_TYPE_NOTICE = 'notice'
4942
4941
4943 NOTIFICATION_LEVEL_INFO = 'info'
4942 NOTIFICATION_LEVEL_INFO = 'info'
4944 NOTIFICATION_LEVEL_WARNING = 'warning'
4943 NOTIFICATION_LEVEL_WARNING = 'warning'
4945 NOTIFICATION_LEVEL_ERROR = 'error'
4944 NOTIFICATION_LEVEL_ERROR = 'error'
4946
4945
4947 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4946 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4948
4947
4949 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4948 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4950 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4949 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4951
4950
4952 notice_read = Column('notice_read', Boolean, default=False)
4951 notice_read = Column('notice_read', Boolean, default=False)
4953
4952
4954 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4953 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4955 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4954 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4956
4955
4957 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4956 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)
4957 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4959
4958
4960 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4959 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4961 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4960 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4962
4961
4963 @classmethod
4962 @classmethod
4964 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4963 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4965
4964
4966 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4965 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4967 cls.NOTIFICATION_LEVEL_WARNING,
4966 cls.NOTIFICATION_LEVEL_WARNING,
4968 cls.NOTIFICATION_LEVEL_INFO]:
4967 cls.NOTIFICATION_LEVEL_INFO]:
4969 return
4968 return
4970
4969
4971 from rhodecode.model.user import UserModel
4970 from rhodecode.model.user import UserModel
4972 user = UserModel().get_user(user)
4971 user = UserModel().get_user(user)
4973
4972
4974 new_notice = UserNotice()
4973 new_notice = UserNotice()
4975 if not allow_duplicate:
4974 if not allow_duplicate:
4976 existing_msg = UserNotice().query() \
4975 existing_msg = UserNotice().query() \
4977 .filter(UserNotice.user == user) \
4976 .filter(UserNotice.user == user) \
4978 .filter(UserNotice.notice_body == body) \
4977 .filter(UserNotice.notice_body == body) \
4979 .filter(UserNotice.notice_read == false()) \
4978 .filter(UserNotice.notice_read == false()) \
4980 .scalar()
4979 .scalar()
4981 if existing_msg:
4980 if existing_msg:
4982 log.warning('Ignoring duplicate notice for user %s', user)
4981 log.warning('Ignoring duplicate notice for user %s', user)
4983 return
4982 return
4984
4983
4985 new_notice.user = user
4984 new_notice.user = user
4986 new_notice.notice_subject = subject
4985 new_notice.notice_subject = subject
4987 new_notice.notice_body = body
4986 new_notice.notice_body = body
4988 new_notice.notification_level = notice_level
4987 new_notice.notification_level = notice_level
4989 Session().add(new_notice)
4988 Session().add(new_notice)
4990 Session().commit()
4989 Session().commit()
4991
4990
4992
4991
4993 class Gist(Base, BaseModel):
4992 class Gist(Base, BaseModel):
4994 __tablename__ = 'gists'
4993 __tablename__ = 'gists'
4995 __table_args__ = (
4994 __table_args__ = (
4996 Index('g_gist_access_id_idx', 'gist_access_id'),
4995 Index('g_gist_access_id_idx', 'gist_access_id'),
4997 Index('g_created_on_idx', 'created_on'),
4996 Index('g_created_on_idx', 'created_on'),
4998 base_table_args
4997 base_table_args
4999 )
4998 )
5000
4999
5001 GIST_PUBLIC = 'public'
5000 GIST_PUBLIC = 'public'
5002 GIST_PRIVATE = 'private'
5001 GIST_PRIVATE = 'private'
5003 DEFAULT_FILENAME = 'gistfile1.txt'
5002 DEFAULT_FILENAME = 'gistfile1.txt'
5004
5003
5005 ACL_LEVEL_PUBLIC = 'acl_public'
5004 ACL_LEVEL_PUBLIC = 'acl_public'
5006 ACL_LEVEL_PRIVATE = 'acl_private'
5005 ACL_LEVEL_PRIVATE = 'acl_private'
5007
5006
5008 gist_id = Column('gist_id', Integer(), primary_key=True)
5007 gist_id = Column('gist_id', Integer(), primary_key=True)
5009 gist_access_id = Column('gist_access_id', Unicode(250))
5008 gist_access_id = Column('gist_access_id', Unicode(250))
5010 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5009 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
5011 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5010 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
5012 gist_expires = Column('gist_expires', Float(53), nullable=False)
5011 gist_expires = Column('gist_expires', Float(53), nullable=False)
5013 gist_type = Column('gist_type', Unicode(128), nullable=False)
5012 gist_type = Column('gist_type', Unicode(128), nullable=False)
5014 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5013 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)
5014 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5016 acl_level = Column('acl_level', Unicode(128), nullable=True)
5015 acl_level = Column('acl_level', Unicode(128), nullable=True)
5017
5016
5018 owner = relationship('User', back_populates='user_gists')
5017 owner = relationship('User', back_populates='user_gists')
5019
5018
5020 def __repr__(self):
5019 def __repr__(self):
5021 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5020 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
5022
5021
5023 @hybrid_property
5022 @hybrid_property
5024 def description_safe(self):
5023 def description_safe(self):
5025 from rhodecode.lib import helpers as h
5024 from rhodecode.lib import helpers as h
5026 return h.escape(self.gist_description)
5025 return h.escape(self.gist_description)
5027
5026
5028 @classmethod
5027 @classmethod
5029 def get_or_404(cls, id_):
5028 def get_or_404(cls, id_):
5030 from pyramid.httpexceptions import HTTPNotFound
5029 from pyramid.httpexceptions import HTTPNotFound
5031
5030
5032 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5031 res = cls.query().filter(cls.gist_access_id == id_).scalar()
5033 if not res:
5032 if not res:
5034 log.debug('WARN: No DB entry with id %s', id_)
5033 log.debug('WARN: No DB entry with id %s', id_)
5035 raise HTTPNotFound()
5034 raise HTTPNotFound()
5036 return res
5035 return res
5037
5036
5038 @classmethod
5037 @classmethod
5039 def get_by_access_id(cls, gist_access_id):
5038 def get_by_access_id(cls, gist_access_id):
5040 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5039 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
5041
5040
5042 def gist_url(self):
5041 def gist_url(self):
5043 from rhodecode.model.gist import GistModel
5042 from rhodecode.model.gist import GistModel
5044 return GistModel().get_url(self)
5043 return GistModel().get_url(self)
5045
5044
5046 @classmethod
5045 @classmethod
5047 def base_path(cls):
5046 def base_path(cls):
5048 """
5047 """
5049 Returns base path when all gists are stored
5048 Returns base path when all gists are stored
5050
5049
5051 :param cls:
5050 :param cls:
5052 """
5051 """
5053 from rhodecode.model.gist import GIST_STORE_LOC
5052 from rhodecode.model.gist import GIST_STORE_LOC
5054 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5053 from rhodecode.lib.utils import get_rhodecode_repo_store_path
5055 repo_store_path = get_rhodecode_repo_store_path()
5054 repo_store_path = get_rhodecode_repo_store_path()
5056 return os.path.join(repo_store_path, GIST_STORE_LOC)
5055 return os.path.join(repo_store_path, GIST_STORE_LOC)
5057
5056
5058 def get_api_data(self):
5057 def get_api_data(self):
5059 """
5058 """
5060 Common function for generating gist related data for API
5059 Common function for generating gist related data for API
5061 """
5060 """
5062 gist = self
5061 gist = self
5063 data = {
5062 data = {
5064 'gist_id': gist.gist_id,
5063 'gist_id': gist.gist_id,
5065 'type': gist.gist_type,
5064 'type': gist.gist_type,
5066 'access_id': gist.gist_access_id,
5065 'access_id': gist.gist_access_id,
5067 'description': gist.gist_description,
5066 'description': gist.gist_description,
5068 'url': gist.gist_url(),
5067 'url': gist.gist_url(),
5069 'expires': gist.gist_expires,
5068 'expires': gist.gist_expires,
5070 'created_on': gist.created_on,
5069 'created_on': gist.created_on,
5071 'modified_at': gist.modified_at,
5070 'modified_at': gist.modified_at,
5072 'content': None,
5071 'content': None,
5073 'acl_level': gist.acl_level,
5072 'acl_level': gist.acl_level,
5074 }
5073 }
5075 return data
5074 return data
5076
5075
5077 def __json__(self):
5076 def __json__(self):
5078 data = dict()
5077 data = dict()
5079 data.update(self.get_api_data())
5078 data.update(self.get_api_data())
5080 return data
5079 return data
5081 # SCM functions
5080 # SCM functions
5082
5081
5083 def scm_instance(self, **kwargs):
5082 def scm_instance(self, **kwargs):
5084 """
5083 """
5085 Get an instance of VCS Repository
5084 Get an instance of VCS Repository
5086
5085
5087 :param kwargs:
5086 :param kwargs:
5088 """
5087 """
5089 from rhodecode.model.gist import GistModel
5088 from rhodecode.model.gist import GistModel
5090 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5089 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
5091 return get_vcs_instance(
5090 return get_vcs_instance(
5092 repo_path=safe_str(full_repo_path), create=False,
5091 repo_path=safe_str(full_repo_path), create=False,
5093 _vcs_alias=GistModel.vcs_backend)
5092 _vcs_alias=GistModel.vcs_backend)
5094
5093
5095
5094
5096 class ExternalIdentity(Base, BaseModel):
5095 class ExternalIdentity(Base, BaseModel):
5097 __tablename__ = 'external_identities'
5096 __tablename__ = 'external_identities'
5098 __table_args__ = (
5097 __table_args__ = (
5099 Index('local_user_id_idx', 'local_user_id'),
5098 Index('local_user_id_idx', 'local_user_id'),
5100 Index('external_id_idx', 'external_id'),
5099 Index('external_id_idx', 'external_id'),
5101 base_table_args
5100 base_table_args
5102 )
5101 )
5103
5102
5104 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5103 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
5105 external_username = Column('external_username', Unicode(1024), default='')
5104 external_username = Column('external_username', Unicode(1024), default='')
5106 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
5105 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)
5106 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
5108 access_token = Column('access_token', String(1024), default='')
5107 access_token = Column('access_token', String(1024), default='')
5109 alt_token = Column('alt_token', String(1024), default='')
5108 alt_token = Column('alt_token', String(1024), default='')
5110 token_secret = Column('token_secret', String(1024), default='')
5109 token_secret = Column('token_secret', String(1024), default='')
5111
5110
5112 @classmethod
5111 @classmethod
5113 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5112 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
5114 """
5113 """
5115 Returns ExternalIdentity instance based on search params
5114 Returns ExternalIdentity instance based on search params
5116
5115
5117 :param external_id:
5116 :param external_id:
5118 :param provider_name:
5117 :param provider_name:
5119 :return: ExternalIdentity
5118 :return: ExternalIdentity
5120 """
5119 """
5121 query = cls.query()
5120 query = cls.query()
5122 query = query.filter(cls.external_id == external_id)
5121 query = query.filter(cls.external_id == external_id)
5123 query = query.filter(cls.provider_name == provider_name)
5122 query = query.filter(cls.provider_name == provider_name)
5124 if local_user_id:
5123 if local_user_id:
5125 query = query.filter(cls.local_user_id == local_user_id)
5124 query = query.filter(cls.local_user_id == local_user_id)
5126 return query.first()
5125 return query.first()
5127
5126
5128 @classmethod
5127 @classmethod
5129 def user_by_external_id_and_provider(cls, external_id, provider_name):
5128 def user_by_external_id_and_provider(cls, external_id, provider_name):
5130 """
5129 """
5131 Returns User instance based on search params
5130 Returns User instance based on search params
5132
5131
5133 :param external_id:
5132 :param external_id:
5134 :param provider_name:
5133 :param provider_name:
5135 :return: User
5134 :return: User
5136 """
5135 """
5137 query = User.query()
5136 query = User.query()
5138 query = query.filter(cls.external_id == external_id)
5137 query = query.filter(cls.external_id == external_id)
5139 query = query.filter(cls.provider_name == provider_name)
5138 query = query.filter(cls.provider_name == provider_name)
5140 query = query.filter(User.user_id == cls.local_user_id)
5139 query = query.filter(User.user_id == cls.local_user_id)
5141 return query.first()
5140 return query.first()
5142
5141
5143 @classmethod
5142 @classmethod
5144 def by_local_user_id(cls, local_user_id):
5143 def by_local_user_id(cls, local_user_id):
5145 """
5144 """
5146 Returns all tokens for user
5145 Returns all tokens for user
5147
5146
5148 :param local_user_id:
5147 :param local_user_id:
5149 :return: ExternalIdentity
5148 :return: ExternalIdentity
5150 """
5149 """
5151 query = cls.query()
5150 query = cls.query()
5152 query = query.filter(cls.local_user_id == local_user_id)
5151 query = query.filter(cls.local_user_id == local_user_id)
5153 return query
5152 return query
5154
5153
5155 @classmethod
5154 @classmethod
5156 def load_provider_plugin(cls, plugin_id):
5155 def load_provider_plugin(cls, plugin_id):
5157 from rhodecode.authentication.base import loadplugin
5156 from rhodecode.authentication.base import loadplugin
5158 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5157 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5159 auth_plugin = loadplugin(_plugin_id)
5158 auth_plugin = loadplugin(_plugin_id)
5160 return auth_plugin
5159 return auth_plugin
5161
5160
5162
5161
5163 class Integration(Base, BaseModel):
5162 class Integration(Base, BaseModel):
5164 __tablename__ = 'integrations'
5163 __tablename__ = 'integrations'
5165 __table_args__ = (
5164 __table_args__ = (
5166 base_table_args
5165 base_table_args
5167 )
5166 )
5168
5167
5169 integration_id = Column('integration_id', Integer(), primary_key=True)
5168 integration_id = Column('integration_id', Integer(), primary_key=True)
5170 integration_type = Column('integration_type', String(255))
5169 integration_type = Column('integration_type', String(255))
5171 enabled = Column('enabled', Boolean(), nullable=False)
5170 enabled = Column('enabled', Boolean(), nullable=False)
5172 name = Column('name', String(255), nullable=False)
5171 name = Column('name', String(255), nullable=False)
5173 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5172 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5174
5173
5175 settings = Column(
5174 settings = Column(
5176 'settings_json', MutationObj.as_mutable(
5175 'settings_json', MutationObj.as_mutable(
5177 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5176 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5178 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5177 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')
5178 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5180
5179
5181 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5180 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')
5181 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5183
5182
5184 @property
5183 @property
5185 def scope(self):
5184 def scope(self):
5186 if self.repo:
5185 if self.repo:
5187 return repr(self.repo)
5186 return repr(self.repo)
5188 if self.repo_group:
5187 if self.repo_group:
5189 if self.child_repos_only:
5188 if self.child_repos_only:
5190 return repr(self.repo_group) + ' (child repos only)'
5189 return repr(self.repo_group) + ' (child repos only)'
5191 else:
5190 else:
5192 return repr(self.repo_group) + ' (recursive)'
5191 return repr(self.repo_group) + ' (recursive)'
5193 if self.child_repos_only:
5192 if self.child_repos_only:
5194 return 'root_repos'
5193 return 'root_repos'
5195 return 'global'
5194 return 'global'
5196
5195
5197 def __repr__(self):
5196 def __repr__(self):
5198 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5197 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5199
5198
5200
5199
5201 class RepoReviewRuleUser(Base, BaseModel):
5200 class RepoReviewRuleUser(Base, BaseModel):
5202 __tablename__ = 'repo_review_rules_users'
5201 __tablename__ = 'repo_review_rules_users'
5203 __table_args__ = (
5202 __table_args__ = (
5204 base_table_args
5203 base_table_args
5205 )
5204 )
5206 ROLE_REVIEWER = 'reviewer'
5205 ROLE_REVIEWER = 'reviewer'
5207 ROLE_OBSERVER = 'observer'
5206 ROLE_OBSERVER = 'observer'
5208 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5207 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5209
5208
5210 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5209 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'))
5210 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)
5211 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5213 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5212 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5214 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5213 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5215 user = relationship('User', back_populates='user_review_rules')
5214 user = relationship('User', back_populates='user_review_rules')
5216
5215
5217 def rule_data(self):
5216 def rule_data(self):
5218 return {
5217 return {
5219 'mandatory': self.mandatory,
5218 'mandatory': self.mandatory,
5220 'role': self.role,
5219 'role': self.role,
5221 }
5220 }
5222
5221
5223
5222
5224 class RepoReviewRuleUserGroup(Base, BaseModel):
5223 class RepoReviewRuleUserGroup(Base, BaseModel):
5225 __tablename__ = 'repo_review_rules_users_groups'
5224 __tablename__ = 'repo_review_rules_users_groups'
5226 __table_args__ = (
5225 __table_args__ = (
5227 base_table_args
5226 base_table_args
5228 )
5227 )
5229
5228
5230 VOTE_RULE_ALL = -1
5229 VOTE_RULE_ALL = -1
5231 ROLE_REVIEWER = 'reviewer'
5230 ROLE_REVIEWER = 'reviewer'
5232 ROLE_OBSERVER = 'observer'
5231 ROLE_OBSERVER = 'observer'
5233 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5232 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5234
5233
5235 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5234 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'))
5235 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)
5236 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)
5237 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5239 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5238 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5240 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5239 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5241 users_group = relationship('UserGroup')
5240 users_group = relationship('UserGroup')
5242
5241
5243 def rule_data(self):
5242 def rule_data(self):
5244 return {
5243 return {
5245 'mandatory': self.mandatory,
5244 'mandatory': self.mandatory,
5246 'role': self.role,
5245 'role': self.role,
5247 'vote_rule': self.vote_rule
5246 'vote_rule': self.vote_rule
5248 }
5247 }
5249
5248
5250 @property
5249 @property
5251 def vote_rule_label(self):
5250 def vote_rule_label(self):
5252 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5251 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5253 return 'all must vote'
5252 return 'all must vote'
5254 else:
5253 else:
5255 return 'min. vote {}'.format(self.vote_rule)
5254 return 'min. vote {}'.format(self.vote_rule)
5256
5255
5257
5256
5258 class RepoReviewRule(Base, BaseModel):
5257 class RepoReviewRule(Base, BaseModel):
5259 __tablename__ = 'repo_review_rules'
5258 __tablename__ = 'repo_review_rules'
5260 __table_args__ = (
5259 __table_args__ = (
5261 base_table_args
5260 base_table_args
5262 )
5261 )
5263
5262
5264 repo_review_rule_id = Column(
5263 repo_review_rule_id = Column(
5265 'repo_review_rule_id', Integer(), primary_key=True)
5264 'repo_review_rule_id', Integer(), primary_key=True)
5266 repo_id = Column(
5265 repo_id = Column(
5267 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5266 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5268 repo = relationship('Repository', back_populates='review_rules')
5267 repo = relationship('Repository', back_populates='review_rules')
5269
5268
5270 review_rule_name = Column('review_rule_name', String(255))
5269 review_rule_name = Column('review_rule_name', String(255))
5271 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5270 _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
5271 _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
5272 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5274
5273
5275 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5274 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5276
5275
5277 # Legacy fields, just for backward compat
5276 # Legacy fields, just for backward compat
5278 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5277 _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)
5278 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5280
5279
5281 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5280 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)
5281 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5283
5282
5284 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5283 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5285
5284
5286 rule_users = relationship('RepoReviewRuleUser')
5285 rule_users = relationship('RepoReviewRuleUser')
5287 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5286 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5288
5287
5289 def _validate_pattern(self, value):
5288 def _validate_pattern(self, value):
5290 re.compile('^' + glob2re(value) + '$')
5289 re.compile('^' + glob2re(value) + '$')
5291
5290
5292 @hybrid_property
5291 @hybrid_property
5293 def source_branch_pattern(self):
5292 def source_branch_pattern(self):
5294 return self._branch_pattern or '*'
5293 return self._branch_pattern or '*'
5295
5294
5296 @source_branch_pattern.setter
5295 @source_branch_pattern.setter
5297 def source_branch_pattern(self, value):
5296 def source_branch_pattern(self, value):
5298 self._validate_pattern(value)
5297 self._validate_pattern(value)
5299 self._branch_pattern = value or '*'
5298 self._branch_pattern = value or '*'
5300
5299
5301 @hybrid_property
5300 @hybrid_property
5302 def target_branch_pattern(self):
5301 def target_branch_pattern(self):
5303 return self._target_branch_pattern or '*'
5302 return self._target_branch_pattern or '*'
5304
5303
5305 @target_branch_pattern.setter
5304 @target_branch_pattern.setter
5306 def target_branch_pattern(self, value):
5305 def target_branch_pattern(self, value):
5307 self._validate_pattern(value)
5306 self._validate_pattern(value)
5308 self._target_branch_pattern = value or '*'
5307 self._target_branch_pattern = value or '*'
5309
5308
5310 @hybrid_property
5309 @hybrid_property
5311 def file_pattern(self):
5310 def file_pattern(self):
5312 return self._file_pattern or '*'
5311 return self._file_pattern or '*'
5313
5312
5314 @file_pattern.setter
5313 @file_pattern.setter
5315 def file_pattern(self, value):
5314 def file_pattern(self, value):
5316 self._validate_pattern(value)
5315 self._validate_pattern(value)
5317 self._file_pattern = value or '*'
5316 self._file_pattern = value or '*'
5318
5317
5319 @hybrid_property
5318 @hybrid_property
5320 def forbid_pr_author_to_review(self):
5319 def forbid_pr_author_to_review(self):
5321 return self.pr_author == 'forbid_pr_author'
5320 return self.pr_author == 'forbid_pr_author'
5322
5321
5323 @hybrid_property
5322 @hybrid_property
5324 def include_pr_author_to_review(self):
5323 def include_pr_author_to_review(self):
5325 return self.pr_author == 'include_pr_author'
5324 return self.pr_author == 'include_pr_author'
5326
5325
5327 @hybrid_property
5326 @hybrid_property
5328 def forbid_commit_author_to_review(self):
5327 def forbid_commit_author_to_review(self):
5329 return self.commit_author == 'forbid_commit_author'
5328 return self.commit_author == 'forbid_commit_author'
5330
5329
5331 @hybrid_property
5330 @hybrid_property
5332 def include_commit_author_to_review(self):
5331 def include_commit_author_to_review(self):
5333 return self.commit_author == 'include_commit_author'
5332 return self.commit_author == 'include_commit_author'
5334
5333
5335 def matches(self, source_branch, target_branch, files_changed):
5334 def matches(self, source_branch, target_branch, files_changed):
5336 """
5335 """
5337 Check if this review rule matches a branch/files in a pull request
5336 Check if this review rule matches a branch/files in a pull request
5338
5337
5339 :param source_branch: source branch name for the commit
5338 :param source_branch: source branch name for the commit
5340 :param target_branch: target branch name for the commit
5339 :param target_branch: target branch name for the commit
5341 :param files_changed: list of file paths changed in the pull request
5340 :param files_changed: list of file paths changed in the pull request
5342 """
5341 """
5343
5342
5344 source_branch = source_branch or ''
5343 source_branch = source_branch or ''
5345 target_branch = target_branch or ''
5344 target_branch = target_branch or ''
5346 files_changed = files_changed or []
5345 files_changed = files_changed or []
5347
5346
5348 branch_matches = True
5347 branch_matches = True
5349 if source_branch or target_branch:
5348 if source_branch or target_branch:
5350 if self.source_branch_pattern == '*':
5349 if self.source_branch_pattern == '*':
5351 source_branch_match = True
5350 source_branch_match = True
5352 else:
5351 else:
5353 if self.source_branch_pattern.startswith('re:'):
5352 if self.source_branch_pattern.startswith('re:'):
5354 source_pattern = self.source_branch_pattern[3:]
5353 source_pattern = self.source_branch_pattern[3:]
5355 else:
5354 else:
5356 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5355 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5357 source_branch_regex = re.compile(source_pattern)
5356 source_branch_regex = re.compile(source_pattern)
5358 source_branch_match = bool(source_branch_regex.search(source_branch))
5357 source_branch_match = bool(source_branch_regex.search(source_branch))
5359 if self.target_branch_pattern == '*':
5358 if self.target_branch_pattern == '*':
5360 target_branch_match = True
5359 target_branch_match = True
5361 else:
5360 else:
5362 if self.target_branch_pattern.startswith('re:'):
5361 if self.target_branch_pattern.startswith('re:'):
5363 target_pattern = self.target_branch_pattern[3:]
5362 target_pattern = self.target_branch_pattern[3:]
5364 else:
5363 else:
5365 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5364 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5366 target_branch_regex = re.compile(target_pattern)
5365 target_branch_regex = re.compile(target_pattern)
5367 target_branch_match = bool(target_branch_regex.search(target_branch))
5366 target_branch_match = bool(target_branch_regex.search(target_branch))
5368
5367
5369 branch_matches = source_branch_match and target_branch_match
5368 branch_matches = source_branch_match and target_branch_match
5370
5369
5371 files_matches = True
5370 files_matches = True
5372 if self.file_pattern != '*':
5371 if self.file_pattern != '*':
5373 files_matches = False
5372 files_matches = False
5374 if self.file_pattern.startswith('re:'):
5373 if self.file_pattern.startswith('re:'):
5375 file_pattern = self.file_pattern[3:]
5374 file_pattern = self.file_pattern[3:]
5376 else:
5375 else:
5377 file_pattern = glob2re(self.file_pattern)
5376 file_pattern = glob2re(self.file_pattern)
5378 file_regex = re.compile(file_pattern)
5377 file_regex = re.compile(file_pattern)
5379 for file_data in files_changed:
5378 for file_data in files_changed:
5380 filename = file_data.get('filename')
5379 filename = file_data.get('filename')
5381
5380
5382 if file_regex.search(filename):
5381 if file_regex.search(filename):
5383 files_matches = True
5382 files_matches = True
5384 break
5383 break
5385
5384
5386 return branch_matches and files_matches
5385 return branch_matches and files_matches
5387
5386
5388 @property
5387 @property
5389 def review_users(self):
5388 def review_users(self):
5390 """ Returns the users which this rule applies to """
5389 """ Returns the users which this rule applies to """
5391
5390
5392 users = collections.OrderedDict()
5391 users = collections.OrderedDict()
5393
5392
5394 for rule_user in self.rule_users:
5393 for rule_user in self.rule_users:
5395 if rule_user.user.active:
5394 if rule_user.user.active:
5396 if rule_user.user not in users:
5395 if rule_user.user not in users:
5397 users[rule_user.user.username] = {
5396 users[rule_user.user.username] = {
5398 'user': rule_user.user,
5397 'user': rule_user.user,
5399 'source': 'user',
5398 'source': 'user',
5400 'source_data': {},
5399 'source_data': {},
5401 'data': rule_user.rule_data()
5400 'data': rule_user.rule_data()
5402 }
5401 }
5403
5402
5404 for rule_user_group in self.rule_user_groups:
5403 for rule_user_group in self.rule_user_groups:
5405 source_data = {
5404 source_data = {
5406 'user_group_id': rule_user_group.users_group.users_group_id,
5405 'user_group_id': rule_user_group.users_group.users_group_id,
5407 'name': rule_user_group.users_group.users_group_name,
5406 'name': rule_user_group.users_group.users_group_name,
5408 'members': len(rule_user_group.users_group.members)
5407 'members': len(rule_user_group.users_group.members)
5409 }
5408 }
5410 for member in rule_user_group.users_group.members:
5409 for member in rule_user_group.users_group.members:
5411 if member.user.active:
5410 if member.user.active:
5412 key = member.user.username
5411 key = member.user.username
5413 if key in users:
5412 if key in users:
5414 # skip this member as we have him already
5413 # skip this member as we have him already
5415 # this prevents from override the "first" matched
5414 # this prevents from override the "first" matched
5416 # users with duplicates in multiple groups
5415 # users with duplicates in multiple groups
5417 continue
5416 continue
5418
5417
5419 users[key] = {
5418 users[key] = {
5420 'user': member.user,
5419 'user': member.user,
5421 'source': 'user_group',
5420 'source': 'user_group',
5422 'source_data': source_data,
5421 'source_data': source_data,
5423 'data': rule_user_group.rule_data()
5422 'data': rule_user_group.rule_data()
5424 }
5423 }
5425
5424
5426 return users
5425 return users
5427
5426
5428 def user_group_vote_rule(self, user_id):
5427 def user_group_vote_rule(self, user_id):
5429
5428
5430 rules = []
5429 rules = []
5431 if not self.rule_user_groups:
5430 if not self.rule_user_groups:
5432 return rules
5431 return rules
5433
5432
5434 for user_group in self.rule_user_groups:
5433 for user_group in self.rule_user_groups:
5435 user_group_members = [x.user_id for x in user_group.users_group.members]
5434 user_group_members = [x.user_id for x in user_group.users_group.members]
5436 if user_id in user_group_members:
5435 if user_id in user_group_members:
5437 rules.append(user_group)
5436 rules.append(user_group)
5438 return rules
5437 return rules
5439
5438
5440 def __repr__(self):
5439 def __repr__(self):
5441 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5440 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5442
5441
5443
5442
5444 class ScheduleEntry(Base, BaseModel):
5443 class ScheduleEntry(Base, BaseModel):
5445 __tablename__ = 'schedule_entries'
5444 __tablename__ = 'schedule_entries'
5446 __table_args__ = (
5445 __table_args__ = (
5447 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5446 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5448 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5447 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5449 base_table_args,
5448 base_table_args,
5450 )
5449 )
5451 SCHEDULE_TYPE_INTEGER = "integer"
5450 SCHEDULE_TYPE_INTEGER = "integer"
5452 SCHEDULE_TYPE_CRONTAB = "crontab"
5451 SCHEDULE_TYPE_CRONTAB = "crontab"
5453
5452
5454 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5453 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5455 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5454 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5456
5455
5457 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5456 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)
5457 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)
5458 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5460
5459
5461 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5460 _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()))))
5461 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5463
5462
5464 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5463 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)
5464 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5466
5465
5467 # task
5466 # task
5468 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5467 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)
5468 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()))))
5469 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()))))
5470 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5472
5471
5473 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5472 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)
5473 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5475
5474
5476 @hybrid_property
5475 @hybrid_property
5477 def schedule_type(self):
5476 def schedule_type(self):
5478 return self._schedule_type
5477 return self._schedule_type
5479
5478
5480 @schedule_type.setter
5479 @schedule_type.setter
5481 def schedule_type(self, val):
5480 def schedule_type(self, val):
5482 if val not in self.schedule_types:
5481 if val not in self.schedule_types:
5483 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5482 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5484 val, self.schedule_type))
5483 val, self.schedule_type))
5485
5484
5486 self._schedule_type = val
5485 self._schedule_type = val
5487
5486
5488 @classmethod
5487 @classmethod
5489 def get_uid(cls, obj):
5488 def get_uid(cls, obj):
5490 args = obj.task_args
5489 args = obj.task_args
5491 kwargs = obj.task_kwargs
5490 kwargs = obj.task_kwargs
5492 if isinstance(args, JsonRaw):
5491 if isinstance(args, JsonRaw):
5493 try:
5492 try:
5494 args = json.loads(args)
5493 args = json.loads(args)
5495 except ValueError:
5494 except ValueError:
5496 args = tuple()
5495 args = tuple()
5497
5496
5498 if isinstance(kwargs, JsonRaw):
5497 if isinstance(kwargs, JsonRaw):
5499 try:
5498 try:
5500 kwargs = json.loads(kwargs)
5499 kwargs = json.loads(kwargs)
5501 except ValueError:
5500 except ValueError:
5502 kwargs = dict()
5501 kwargs = dict()
5503
5502
5504 dot_notation = obj.task_dot_notation
5503 dot_notation = obj.task_dot_notation
5505 val = '.'.join(map(safe_str, [
5504 val = '.'.join(map(safe_str, [
5506 sorted(dot_notation), args, sorted(kwargs.items())]))
5505 sorted(dot_notation), args, sorted(kwargs.items())]))
5507 return sha1(safe_bytes(val))
5506 return sha1(safe_bytes(val))
5508
5507
5509 @classmethod
5508 @classmethod
5510 def get_by_schedule_name(cls, schedule_name):
5509 def get_by_schedule_name(cls, schedule_name):
5511 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5510 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5512
5511
5513 @classmethod
5512 @classmethod
5514 def get_by_schedule_id(cls, schedule_id):
5513 def get_by_schedule_id(cls, schedule_id):
5515 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5514 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5516
5515
5517 @property
5516 @property
5518 def task(self):
5517 def task(self):
5519 return self.task_dot_notation
5518 return self.task_dot_notation
5520
5519
5521 @property
5520 @property
5522 def schedule(self):
5521 def schedule(self):
5523 from rhodecode.lib.celerylib.utils import raw_2_schedule
5522 from rhodecode.lib.celerylib.utils import raw_2_schedule
5524 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5523 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5525 return schedule
5524 return schedule
5526
5525
5527 @property
5526 @property
5528 def args(self):
5527 def args(self):
5529 try:
5528 try:
5530 return list(self.task_args or [])
5529 return list(self.task_args or [])
5531 except ValueError:
5530 except ValueError:
5532 return list()
5531 return list()
5533
5532
5534 @property
5533 @property
5535 def kwargs(self):
5534 def kwargs(self):
5536 try:
5535 try:
5537 return dict(self.task_kwargs or {})
5536 return dict(self.task_kwargs or {})
5538 except ValueError:
5537 except ValueError:
5539 return dict()
5538 return dict()
5540
5539
5541 def _as_raw(self, val, indent=False):
5540 def _as_raw(self, val, indent=False):
5542 if hasattr(val, 'de_coerce'):
5541 if hasattr(val, 'de_coerce'):
5543 val = val.de_coerce()
5542 val = val.de_coerce()
5544 if val:
5543 if val:
5545 if indent:
5544 if indent:
5546 val = ext_json.formatted_str_json(val)
5545 val = ext_json.formatted_str_json(val)
5547 else:
5546 else:
5548 val = ext_json.str_json(val)
5547 val = ext_json.str_json(val)
5549
5548
5550 return val
5549 return val
5551
5550
5552 @property
5551 @property
5553 def schedule_definition_raw(self):
5552 def schedule_definition_raw(self):
5554 return self._as_raw(self.schedule_definition)
5553 return self._as_raw(self.schedule_definition)
5555
5554
5556 def args_raw(self, indent=False):
5555 def args_raw(self, indent=False):
5557 return self._as_raw(self.task_args, indent)
5556 return self._as_raw(self.task_args, indent)
5558
5557
5559 def kwargs_raw(self, indent=False):
5558 def kwargs_raw(self, indent=False):
5560 return self._as_raw(self.task_kwargs, indent)
5559 return self._as_raw(self.task_kwargs, indent)
5561
5560
5562 def __repr__(self):
5561 def __repr__(self):
5563 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5562 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5564
5563
5565
5564
5566 @event.listens_for(ScheduleEntry, 'before_update')
5565 @event.listens_for(ScheduleEntry, 'before_update')
5567 def update_task_uid(mapper, connection, target):
5566 def update_task_uid(mapper, connection, target):
5568 target.task_uid = ScheduleEntry.get_uid(target)
5567 target.task_uid = ScheduleEntry.get_uid(target)
5569
5568
5570
5569
5571 @event.listens_for(ScheduleEntry, 'before_insert')
5570 @event.listens_for(ScheduleEntry, 'before_insert')
5572 def set_task_uid(mapper, connection, target):
5571 def set_task_uid(mapper, connection, target):
5573 target.task_uid = ScheduleEntry.get_uid(target)
5572 target.task_uid = ScheduleEntry.get_uid(target)
5574
5573
5575
5574
5576 class _BaseBranchPerms(BaseModel):
5575 class _BaseBranchPerms(BaseModel):
5577 @classmethod
5576 @classmethod
5578 def compute_hash(cls, value):
5577 def compute_hash(cls, value):
5579 return sha1_safe(value)
5578 return sha1_safe(value)
5580
5579
5581 @hybrid_property
5580 @hybrid_property
5582 def branch_pattern(self):
5581 def branch_pattern(self):
5583 return self._branch_pattern or '*'
5582 return self._branch_pattern or '*'
5584
5583
5585 @hybrid_property
5584 @hybrid_property
5586 def branch_hash(self):
5585 def branch_hash(self):
5587 return self._branch_hash
5586 return self._branch_hash
5588
5587
5589 def _validate_glob(self, value):
5588 def _validate_glob(self, value):
5590 re.compile('^' + glob2re(value) + '$')
5589 re.compile('^' + glob2re(value) + '$')
5591
5590
5592 @branch_pattern.setter
5591 @branch_pattern.setter
5593 def branch_pattern(self, value):
5592 def branch_pattern(self, value):
5594 self._validate_glob(value)
5593 self._validate_glob(value)
5595 self._branch_pattern = value or '*'
5594 self._branch_pattern = value or '*'
5596 # set the Hash when setting the branch pattern
5595 # set the Hash when setting the branch pattern
5597 self._branch_hash = self.compute_hash(self._branch_pattern)
5596 self._branch_hash = self.compute_hash(self._branch_pattern)
5598
5597
5599 def matches(self, branch):
5598 def matches(self, branch):
5600 """
5599 """
5601 Check if this the branch matches entry
5600 Check if this the branch matches entry
5602
5601
5603 :param branch: branch name for the commit
5602 :param branch: branch name for the commit
5604 """
5603 """
5605
5604
5606 branch = branch or ''
5605 branch = branch or ''
5607
5606
5608 branch_matches = True
5607 branch_matches = True
5609 if branch:
5608 if branch:
5610 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5609 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5611 branch_matches = bool(branch_regex.search(branch))
5610 branch_matches = bool(branch_regex.search(branch))
5612
5611
5613 return branch_matches
5612 return branch_matches
5614
5613
5615
5614
5616 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5615 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5617 __tablename__ = 'user_to_repo_branch_permissions'
5616 __tablename__ = 'user_to_repo_branch_permissions'
5618 __table_args__ = (
5617 __table_args__ = (
5619 base_table_args
5618 base_table_args
5620 )
5619 )
5621
5620
5622 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5621 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5623
5622
5624 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5623 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')
5624 repo = relationship('Repository', back_populates='user_branch_perms')
5626
5625
5627 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5626 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5628 permission = relationship('Permission')
5627 permission = relationship('Permission')
5629
5628
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)
5629 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')
5630 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5632
5631
5633 rule_order = Column('rule_order', Integer(), nullable=False)
5632 rule_order = Column('rule_order', Integer(), nullable=False)
5634 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5633 _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'))
5634 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5636
5635
5637 def __repr__(self):
5636 def __repr__(self):
5638 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5637 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5639
5638
5640
5639
5641 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5640 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5642 __tablename__ = 'user_group_to_repo_branch_permissions'
5641 __tablename__ = 'user_group_to_repo_branch_permissions'
5643 __table_args__ = (
5642 __table_args__ = (
5644 base_table_args
5643 base_table_args
5645 )
5644 )
5646
5645
5647 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5646 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5648
5647
5649 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5648 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')
5649 repo = relationship('Repository', back_populates='user_group_branch_perms')
5651
5650
5652 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5651 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5653 permission = relationship('Permission')
5652 permission = relationship('Permission')
5654
5653
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)
5654 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')
5655 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5657
5656
5658 rule_order = Column('rule_order', Integer(), nullable=False)
5657 rule_order = Column('rule_order', Integer(), nullable=False)
5659 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5658 _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'))
5659 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5661
5660
5662 def __repr__(self):
5661 def __repr__(self):
5663 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5662 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5664
5663
5665
5664
5666 class UserBookmark(Base, BaseModel):
5665 class UserBookmark(Base, BaseModel):
5667 __tablename__ = 'user_bookmarks'
5666 __tablename__ = 'user_bookmarks'
5668 __table_args__ = (
5667 __table_args__ = (
5669 UniqueConstraint('user_id', 'bookmark_repo_id'),
5668 UniqueConstraint('user_id', 'bookmark_repo_id'),
5670 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5669 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5671 UniqueConstraint('user_id', 'bookmark_position'),
5670 UniqueConstraint('user_id', 'bookmark_position'),
5672 base_table_args
5671 base_table_args
5673 )
5672 )
5674
5673
5675 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5674 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)
5675 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5677 position = Column("bookmark_position", Integer(), nullable=False)
5676 position = Column("bookmark_position", Integer(), nullable=False)
5678 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5677 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)
5678 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)
5679 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5681
5680
5682 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5681 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)
5682 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5684
5683
5685 user = relationship("User")
5684 user = relationship("User")
5686
5685
5687 repository = relationship("Repository")
5686 repository = relationship("Repository")
5688 repository_group = relationship("RepoGroup")
5687 repository_group = relationship("RepoGroup")
5689
5688
5690 @classmethod
5689 @classmethod
5691 def get_by_position_for_user(cls, position, user_id):
5690 def get_by_position_for_user(cls, position, user_id):
5692 return cls.query() \
5691 return cls.query() \
5693 .filter(UserBookmark.user_id == user_id) \
5692 .filter(UserBookmark.user_id == user_id) \
5694 .filter(UserBookmark.position == position).scalar()
5693 .filter(UserBookmark.position == position).scalar()
5695
5694
5696 @classmethod
5695 @classmethod
5697 def get_bookmarks_for_user(cls, user_id, cache=True):
5696 def get_bookmarks_for_user(cls, user_id, cache=True):
5698 bookmarks = select(
5697 bookmarks = select(
5699 UserBookmark.title,
5698 UserBookmark.title,
5700 UserBookmark.position,
5699 UserBookmark.position,
5701 ) \
5700 ) \
5702 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5701 .add_columns(Repository.repo_id, Repository.repo_type, Repository.repo_name) \
5703 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5702 .add_columns(RepoGroup.group_id, RepoGroup.group_name) \
5704 .where(UserBookmark.user_id == user_id) \
5703 .where(UserBookmark.user_id == user_id) \
5705 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5704 .outerjoin(Repository, Repository.repo_id == UserBookmark.bookmark_repo_id) \
5706 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5705 .outerjoin(RepoGroup, RepoGroup.group_id == UserBookmark.bookmark_repo_group_id) \
5707 .order_by(UserBookmark.position.asc())
5706 .order_by(UserBookmark.position.asc())
5708
5707
5709 if cache:
5708 if cache:
5710 bookmarks = bookmarks.options(
5709 bookmarks = bookmarks.options(
5711 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5710 FromCache("sql_cache_short", f"get_user_{user_id}_bookmarks")
5712 )
5711 )
5713
5712
5714 return Session().execute(bookmarks).all()
5713 return Session().execute(bookmarks).all()
5715
5714
5716 def __repr__(self):
5715 def __repr__(self):
5717 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5716 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5718
5717
5719
5718
5720 class FileStore(Base, BaseModel):
5719 class FileStore(Base, BaseModel):
5721 __tablename__ = 'file_store'
5720 __tablename__ = 'file_store'
5722 __table_args__ = (
5721 __table_args__ = (
5723 base_table_args
5722 base_table_args
5724 )
5723 )
5725
5724
5726 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5725 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5727 file_uid = Column('file_uid', String(1024), nullable=False)
5726 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)
5727 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)
5728 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)
5729 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5731
5730
5732 # sha256 hash
5731 # sha256 hash
5733 file_hash = Column('file_hash', String(512), nullable=False)
5732 file_hash = Column('file_hash', String(512), nullable=False)
5734 file_size = Column('file_size', BigInteger(), nullable=False)
5733 file_size = Column('file_size', BigInteger(), nullable=False)
5735
5734
5736 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5735 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)
5736 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5738 accessed_count = Column('accessed_count', Integer(), default=0)
5737 accessed_count = Column('accessed_count', Integer(), default=0)
5739
5738
5740 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5739 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5741
5740
5742 # if repo/repo_group reference is set, check for permissions
5741 # if repo/repo_group reference is set, check for permissions
5743 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5742 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5744
5743
5745 # hidden defines an attachment that should be hidden from showing in artifact listing
5744 # hidden defines an attachment that should be hidden from showing in artifact listing
5746 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5745 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5747
5746
5748 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5747 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')
5748 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5750
5749
5751 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5750 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5752
5751
5753 # scope limited to user, which requester have access to
5752 # scope limited to user, which requester have access to
5754 scope_user_id = Column(
5753 scope_user_id = Column(
5755 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5754 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5756 nullable=True, unique=None, default=None)
5755 nullable=True, unique=None, default=None)
5757 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5756 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5758
5757
5759 # scope limited to user group, which requester have access to
5758 # scope limited to user group, which requester have access to
5760 scope_user_group_id = Column(
5759 scope_user_group_id = Column(
5761 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5760 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5762 nullable=True, unique=None, default=None)
5761 nullable=True, unique=None, default=None)
5763 user_group = relationship('UserGroup', lazy='joined')
5762 user_group = relationship('UserGroup', lazy='joined')
5764
5763
5765 # scope limited to repo, which requester have access to
5764 # scope limited to repo, which requester have access to
5766 scope_repo_id = Column(
5765 scope_repo_id = Column(
5767 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5766 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5768 nullable=True, unique=None, default=None)
5767 nullable=True, unique=None, default=None)
5769 repo = relationship('Repository', lazy='joined')
5768 repo = relationship('Repository', lazy='joined')
5770
5769
5771 # scope limited to repo group, which requester have access to
5770 # scope limited to repo group, which requester have access to
5772 scope_repo_group_id = Column(
5771 scope_repo_group_id = Column(
5773 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5772 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5774 nullable=True, unique=None, default=None)
5773 nullable=True, unique=None, default=None)
5775 repo_group = relationship('RepoGroup', lazy='joined')
5774 repo_group = relationship('RepoGroup', lazy='joined')
5776
5775
5777 @classmethod
5776 @classmethod
5778 def get_scope(cls, scope_type, scope_id):
5777 def get_scope(cls, scope_type, scope_id):
5779 if scope_type == 'repo':
5778 if scope_type == 'repo':
5780 return f'repo:{scope_id}'
5779 return f'repo:{scope_id}'
5781 elif scope_type == 'repo-group':
5780 elif scope_type == 'repo-group':
5782 return f'repo-group:{scope_id}'
5781 return f'repo-group:{scope_id}'
5783 elif scope_type == 'user':
5782 elif scope_type == 'user':
5784 return f'user:{scope_id}'
5783 return f'user:{scope_id}'
5785 elif scope_type == 'user-group':
5784 elif scope_type == 'user-group':
5786 return f'user-group:{scope_id}'
5785 return f'user-group:{scope_id}'
5787 else:
5786 else:
5788 return scope_type
5787 return scope_type
5789
5788
5790 @classmethod
5789 @classmethod
5791 def get_by_store_uid(cls, file_store_uid, safe=False):
5790 def get_by_store_uid(cls, file_store_uid, safe=False):
5792 if safe:
5791 if safe:
5793 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5792 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5794 else:
5793 else:
5795 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5794 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5796
5795
5797 @classmethod
5796 @classmethod
5798 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5797 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5799 file_description='', enabled=True, hidden=False, check_acl=True,
5798 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):
5799 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5801
5800
5802 store_entry = FileStore()
5801 store_entry = FileStore()
5803 store_entry.file_uid = file_uid
5802 store_entry.file_uid = file_uid
5804 store_entry.file_display_name = file_display_name
5803 store_entry.file_display_name = file_display_name
5805 store_entry.file_org_name = filename
5804 store_entry.file_org_name = filename
5806 store_entry.file_size = file_size
5805 store_entry.file_size = file_size
5807 store_entry.file_hash = file_hash
5806 store_entry.file_hash = file_hash
5808 store_entry.file_description = file_description
5807 store_entry.file_description = file_description
5809
5808
5810 store_entry.check_acl = check_acl
5809 store_entry.check_acl = check_acl
5811 store_entry.enabled = enabled
5810 store_entry.enabled = enabled
5812 store_entry.hidden = hidden
5811 store_entry.hidden = hidden
5813
5812
5814 store_entry.user_id = user_id
5813 store_entry.user_id = user_id
5815 store_entry.scope_user_id = scope_user_id
5814 store_entry.scope_user_id = scope_user_id
5816 store_entry.scope_repo_id = scope_repo_id
5815 store_entry.scope_repo_id = scope_repo_id
5817 store_entry.scope_repo_group_id = scope_repo_group_id
5816 store_entry.scope_repo_group_id = scope_repo_group_id
5818
5817
5819 return store_entry
5818 return store_entry
5820
5819
5821 @classmethod
5820 @classmethod
5822 def store_metadata(cls, file_store_id, args, commit=True):
5821 def store_metadata(cls, file_store_id, args, commit=True):
5823 file_store = FileStore.get(file_store_id)
5822 file_store = FileStore.get(file_store_id)
5824 if file_store is None:
5823 if file_store is None:
5825 return
5824 return
5826
5825
5827 for section, key, value, value_type in args:
5826 for section, key, value, value_type in args:
5828 has_key = FileStoreMetadata().query() \
5827 has_key = FileStoreMetadata().query() \
5829 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5828 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5830 .filter(FileStoreMetadata.file_store_meta_section == section) \
5829 .filter(FileStoreMetadata.file_store_meta_section == section) \
5831 .filter(FileStoreMetadata.file_store_meta_key == key) \
5830 .filter(FileStoreMetadata.file_store_meta_key == key) \
5832 .scalar()
5831 .scalar()
5833 if has_key:
5832 if has_key:
5834 msg = 'key `{}` already defined under section `{}` for this file.'\
5833 msg = 'key `{}` already defined under section `{}` for this file.'\
5835 .format(key, section)
5834 .format(key, section)
5836 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5835 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5837
5836
5838 # NOTE(marcink): raises ArtifactMetadataBadValueType
5837 # NOTE(marcink): raises ArtifactMetadataBadValueType
5839 FileStoreMetadata.valid_value_type(value_type)
5838 FileStoreMetadata.valid_value_type(value_type)
5840
5839
5841 meta_entry = FileStoreMetadata()
5840 meta_entry = FileStoreMetadata()
5842 meta_entry.file_store = file_store
5841 meta_entry.file_store = file_store
5843 meta_entry.file_store_meta_section = section
5842 meta_entry.file_store_meta_section = section
5844 meta_entry.file_store_meta_key = key
5843 meta_entry.file_store_meta_key = key
5845 meta_entry.file_store_meta_value_type = value_type
5844 meta_entry.file_store_meta_value_type = value_type
5846 meta_entry.file_store_meta_value = value
5845 meta_entry.file_store_meta_value = value
5847
5846
5848 Session().add(meta_entry)
5847 Session().add(meta_entry)
5849
5848
5850 try:
5849 try:
5851 if commit:
5850 if commit:
5852 Session().commit()
5851 Session().commit()
5853 except IntegrityError:
5852 except IntegrityError:
5854 Session().rollback()
5853 Session().rollback()
5855 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5854 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5856
5855
5857 @classmethod
5856 @classmethod
5858 def bump_access_counter(cls, file_uid, commit=True):
5857 def bump_access_counter(cls, file_uid, commit=True):
5859 FileStore().query()\
5858 FileStore().query()\
5860 .filter(FileStore.file_uid == file_uid)\
5859 .filter(FileStore.file_uid == file_uid)\
5861 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5860 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5862 FileStore.accessed_on: datetime.datetime.now()})
5861 FileStore.accessed_on: datetime.datetime.now()})
5863 if commit:
5862 if commit:
5864 Session().commit()
5863 Session().commit()
5865
5864
5866 def __json__(self):
5865 def __json__(self):
5867 data = {
5866 data = {
5868 'filename': self.file_display_name,
5867 'filename': self.file_display_name,
5869 'filename_org': self.file_org_name,
5868 'filename_org': self.file_org_name,
5870 'file_uid': self.file_uid,
5869 'file_uid': self.file_uid,
5871 'description': self.file_description,
5870 'description': self.file_description,
5872 'hidden': self.hidden,
5871 'hidden': self.hidden,
5873 'size': self.file_size,
5872 'size': self.file_size,
5874 'created_on': self.created_on,
5873 'created_on': self.created_on,
5875 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5874 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5876 'downloaded_times': self.accessed_count,
5875 'downloaded_times': self.accessed_count,
5877 'sha256': self.file_hash,
5876 'sha256': self.file_hash,
5878 'metadata': self.file_metadata,
5877 'metadata': self.file_metadata,
5879 }
5878 }
5880
5879
5881 return data
5880 return data
5882
5881
5883 def __repr__(self):
5882 def __repr__(self):
5884 return f'<FileStore({self.file_store_id})>'
5883 return f'<FileStore({self.file_store_id})>'
5885
5884
5886
5885
5887 class FileStoreMetadata(Base, BaseModel):
5886 class FileStoreMetadata(Base, BaseModel):
5888 __tablename__ = 'file_store_metadata'
5887 __tablename__ = 'file_store_metadata'
5889 __table_args__ = (
5888 __table_args__ = (
5890 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5889 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),
5890 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),
5891 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5893 base_table_args
5892 base_table_args
5894 )
5893 )
5895 SETTINGS_TYPES = {
5894 SETTINGS_TYPES = {
5896 'str': safe_str,
5895 'str': safe_str,
5897 'int': safe_int,
5896 'int': safe_int,
5898 'unicode': safe_str,
5897 'unicode': safe_str,
5899 'bool': str2bool,
5898 'bool': str2bool,
5900 'list': functools.partial(aslist, sep=',')
5899 'list': functools.partial(aslist, sep=',')
5901 }
5900 }
5902
5901
5903 file_store_meta_id = Column(
5902 file_store_meta_id = Column(
5904 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5903 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5905 primary_key=True)
5904 primary_key=True)
5906 _file_store_meta_section = Column(
5905 _file_store_meta_section = Column(
5907 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5906 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5908 nullable=True, unique=None, default=None)
5907 nullable=True, unique=None, default=None)
5909 _file_store_meta_section_hash = Column(
5908 _file_store_meta_section_hash = Column(
5910 "file_store_meta_section_hash", String(255),
5909 "file_store_meta_section_hash", String(255),
5911 nullable=True, unique=None, default=None)
5910 nullable=True, unique=None, default=None)
5912 _file_store_meta_key = Column(
5911 _file_store_meta_key = Column(
5913 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5912 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5914 nullable=True, unique=None, default=None)
5913 nullable=True, unique=None, default=None)
5915 _file_store_meta_key_hash = Column(
5914 _file_store_meta_key_hash = Column(
5916 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5915 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5917 _file_store_meta_value = Column(
5916 _file_store_meta_value = Column(
5918 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5917 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5919 nullable=True, unique=None, default=None)
5918 nullable=True, unique=None, default=None)
5920 _file_store_meta_value_type = Column(
5919 _file_store_meta_value_type = Column(
5921 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5920 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5922 default='unicode')
5921 default='unicode')
5923
5922
5924 file_store_id = Column(
5923 file_store_id = Column(
5925 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5924 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5926 nullable=True, unique=None, default=None)
5925 nullable=True, unique=None, default=None)
5927
5926
5928 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5927 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5929
5928
5930 @classmethod
5929 @classmethod
5931 def valid_value_type(cls, value):
5930 def valid_value_type(cls, value):
5932 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5931 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5933 raise ArtifactMetadataBadValueType(
5932 raise ArtifactMetadataBadValueType(
5934 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5933 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5935
5934
5936 @hybrid_property
5935 @hybrid_property
5937 def file_store_meta_section(self):
5936 def file_store_meta_section(self):
5938 return self._file_store_meta_section
5937 return self._file_store_meta_section
5939
5938
5940 @file_store_meta_section.setter
5939 @file_store_meta_section.setter
5941 def file_store_meta_section(self, value):
5940 def file_store_meta_section(self, value):
5942 self._file_store_meta_section = value
5941 self._file_store_meta_section = value
5943 self._file_store_meta_section_hash = _hash_key(value)
5942 self._file_store_meta_section_hash = _hash_key(value)
5944
5943
5945 @hybrid_property
5944 @hybrid_property
5946 def file_store_meta_key(self):
5945 def file_store_meta_key(self):
5947 return self._file_store_meta_key
5946 return self._file_store_meta_key
5948
5947
5949 @file_store_meta_key.setter
5948 @file_store_meta_key.setter
5950 def file_store_meta_key(self, value):
5949 def file_store_meta_key(self, value):
5951 self._file_store_meta_key = value
5950 self._file_store_meta_key = value
5952 self._file_store_meta_key_hash = _hash_key(value)
5951 self._file_store_meta_key_hash = _hash_key(value)
5953
5952
5954 @hybrid_property
5953 @hybrid_property
5955 def file_store_meta_value(self):
5954 def file_store_meta_value(self):
5956 val = self._file_store_meta_value
5955 val = self._file_store_meta_value
5957
5956
5958 if self._file_store_meta_value_type:
5957 if self._file_store_meta_value_type:
5959 # e.g unicode.encrypted == unicode
5958 # e.g unicode.encrypted == unicode
5960 _type = self._file_store_meta_value_type.split('.')[0]
5959 _type = self._file_store_meta_value_type.split('.')[0]
5961 # decode the encrypted value if it's encrypted field type
5960 # decode the encrypted value if it's encrypted field type
5962 if '.encrypted' in self._file_store_meta_value_type:
5961 if '.encrypted' in self._file_store_meta_value_type:
5963 cipher = EncryptedTextValue()
5962 cipher = EncryptedTextValue()
5964 val = safe_str(cipher.process_result_value(val, None))
5963 val = safe_str(cipher.process_result_value(val, None))
5965 # do final type conversion
5964 # do final type conversion
5966 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5965 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5967 val = converter(val)
5966 val = converter(val)
5968
5967
5969 return val
5968 return val
5970
5969
5971 @file_store_meta_value.setter
5970 @file_store_meta_value.setter
5972 def file_store_meta_value(self, val):
5971 def file_store_meta_value(self, val):
5973 val = safe_str(val)
5972 val = safe_str(val)
5974 # encode the encrypted value
5973 # encode the encrypted value
5975 if '.encrypted' in self.file_store_meta_value_type:
5974 if '.encrypted' in self.file_store_meta_value_type:
5976 cipher = EncryptedTextValue()
5975 cipher = EncryptedTextValue()
5977 val = safe_str(cipher.process_bind_param(val, None))
5976 val = safe_str(cipher.process_bind_param(val, None))
5978 self._file_store_meta_value = val
5977 self._file_store_meta_value = val
5979
5978
5980 @hybrid_property
5979 @hybrid_property
5981 def file_store_meta_value_type(self):
5980 def file_store_meta_value_type(self):
5982 return self._file_store_meta_value_type
5981 return self._file_store_meta_value_type
5983
5982
5984 @file_store_meta_value_type.setter
5983 @file_store_meta_value_type.setter
5985 def file_store_meta_value_type(self, val):
5984 def file_store_meta_value_type(self, val):
5986 # e.g unicode.encrypted
5985 # e.g unicode.encrypted
5987 self.valid_value_type(val)
5986 self.valid_value_type(val)
5988 self._file_store_meta_value_type = val
5987 self._file_store_meta_value_type = val
5989
5988
5990 def __json__(self):
5989 def __json__(self):
5991 data = {
5990 data = {
5992 'artifact': self.file_store.file_uid,
5991 'artifact': self.file_store.file_uid,
5993 'section': self.file_store_meta_section,
5992 'section': self.file_store_meta_section,
5994 'key': self.file_store_meta_key,
5993 'key': self.file_store_meta_key,
5995 'value': self.file_store_meta_value,
5994 'value': self.file_store_meta_value,
5996 }
5995 }
5997
5996
5998 return data
5997 return data
5999
5998
6000 def __repr__(self):
5999 def __repr__(self):
6001 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6000 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
6002 self.file_store_meta_key, self.file_store_meta_value)
6001 self.file_store_meta_key, self.file_store_meta_value)
6003
6002
6004
6003
6005 class DbMigrateVersion(Base, BaseModel):
6004 class DbMigrateVersion(Base, BaseModel):
6006 __tablename__ = 'db_migrate_version'
6005 __tablename__ = 'db_migrate_version'
6007 __table_args__ = (
6006 __table_args__ = (
6008 base_table_args,
6007 base_table_args,
6009 )
6008 )
6010
6009
6011 repository_id = Column('repository_id', String(250), primary_key=True)
6010 repository_id = Column('repository_id', String(250), primary_key=True)
6012 repository_path = Column('repository_path', Text)
6011 repository_path = Column('repository_path', Text)
6013 version = Column('version', Integer)
6012 version = Column('version', Integer)
6014
6013
6015 @classmethod
6014 @classmethod
6016 def set_version(cls, version):
6015 def set_version(cls, version):
6017 """
6016 """
6018 Helper for forcing a different version, usually for debugging purposes via ishell.
6017 Helper for forcing a different version, usually for debugging purposes via ishell.
6019 """
6018 """
6020 ver = DbMigrateVersion.query().first()
6019 ver = DbMigrateVersion.query().first()
6021 ver.version = version
6020 ver.version = version
6022 Session().commit()
6021 Session().commit()
6023
6022
6024
6023
6025 class DbSession(Base, BaseModel):
6024 class DbSession(Base, BaseModel):
6026 __tablename__ = 'db_session'
6025 __tablename__ = 'db_session'
6027 __table_args__ = (
6026 __table_args__ = (
6028 base_table_args,
6027 base_table_args,
6029 )
6028 )
6030
6029
6031 def __repr__(self):
6030 def __repr__(self):
6032 return f'<DB:DbSession({self.id})>'
6031 return f'<DB:DbSession({self.id})>'
6033
6032
6034 id = Column('id', Integer())
6033 id = Column('id', Integer())
6035 namespace = Column('namespace', String(255), primary_key=True)
6034 namespace = Column('namespace', String(255), primary_key=True)
6036 accessed = Column('accessed', DateTime, nullable=False)
6035 accessed = Column('accessed', DateTime, nullable=False)
6037 created = Column('created', DateTime, nullable=False)
6036 created = Column('created', DateTime, nullable=False)
6038 data = Column('data', PickleType, nullable=False)
6037 data = Column('data', PickleType, nullable=False)
@@ -1,134 +1,139 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('Authentication Settings')}
4 ${_('Authentication Settings')}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}}
6 &middot; ${h.branding(c.rhodecode_name)}}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
11 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
12 &raquo;
12 &raquo;
13 ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))}
13 ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))}
14 &raquo;
14 &raquo;
15 ${resource.display_name}
15 ${resource.display_name}
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='admin')}
19 ${self.menu_items(active='admin')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.admin_menu(active='authentication')}
23 ${self.admin_menu(active='authentication')}
24 </%def>
24 </%def>
25
25
26 <%def name="main()">
26 <%def name="main()">
27
27
28 <div class="box">
28 <div class="box">
29
29
30 <div class='sidebar-col-wrapper'>
30 <div class='sidebar-col-wrapper'>
31
31
32 <div class="sidebar">
32 <div class="sidebar">
33 <ul class="nav nav-pills nav-stacked">
33 <ul class="nav nav-pills nav-stacked">
34 % for item in resource.get_root().get_nav_list():
34 % for item in resource.get_root().get_nav_list():
35 <li ${('class=active' if item == resource else '')}>
35 <li ${('class=active' if item == resource else '')}>
36 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
36 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
37 </li>
37 </li>
38 % endfor
38 % endfor
39 </ul>
39 </ul>
40 </div>
40 </div>
41
41
42 <div class="main-content-full-width">
42 <div class="main-content-full-width">
43 <div class="panel panel-default">
43 <div class="panel panel-default">
44 <div class="panel-heading">
44 <div class="panel-heading">
45 <h3 class="panel-title">${_('Plugin')}: ${resource.display_name}</h3>
45 <h3 class="panel-title">${_('Plugin')}: ${resource.display_name}</h3>
46 </div>
46 </div>
47 <div class="panel-body">
47 <div class="panel-body">
48 <div class="plugin_form">
48 <div class="plugin_form">
49 <div class="fields">
49 <div class="fields">
50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'), request=request)}
50 ${h.secure_form(request.resource_path(resource, route_name='auth_home'), request=request)}
51 <div class="form">
51 <div class="form">
52
52
53 %for node in plugin.get_settings_schema():
53 %for node in plugin.get_settings_schema():
54 <%
54 <%
55 label_to_type = {'label-checkbox': 'bool', 'label-textarea': 'textarea'}
55 label_to_type = {'label-checkbox': 'bool', 'label-textarea': 'textarea'}
56 %>
56 %>
57
57
58 <div class="field">
58 <div class="field">
59 <div class="label ${label_to_type.get(node.widget)}"><label for="${node.name}">${node.title}</label></div>
59 <div class="label ${label_to_type.get(node.widget)}"><label for="${node.name}">${node.title}</label></div>
60 <div class="input">
60 <div class="input">
61 %if node.widget in ["string", "int", "unicode"]:
61 %if node.widget in ["string", "int", "unicode"]:
62 ${h.text(node.name, defaults.get(node.name), class_="large")}
62 ${h.text(node.name, defaults.get(node.name), class_="large")}
63 %elif node.widget == "password":
63 %elif node.widget == "password":
64 ${h.password(node.name, defaults.get(node.name), class_="large")}
64 ${h.password(node.name, defaults.get(node.name), class_="large")}
65 %elif node.widget == "bool":
65 %elif node.widget == "bool":
66 %if node.name == "global_2fa" and c.rhodecode_edition_id != "EE":
67 <input type="checkbox" disabled/>
68 <%node.description = _('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')%>
69 %else:
66 <div class="checkbox">${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
70 <div class="checkbox" >${h.checkbox(node.name, True, checked=defaults.get(node.name))}</div>
71 %endif
67 %elif node.widget == "select":
72 %elif node.widget == "select":
68 ${h.select(node.name, defaults.get(node.name), node.validator.choices, class_="select2AuthSetting")}
73 ${h.select(node.name, defaults.get(node.name), node.validator.choices, class_="select2AuthSetting")}
69 %elif node.widget == "select_with_labels":
74 %elif node.widget == "select_with_labels":
70 ${h.select(node.name, defaults.get(node.name), node.choices, class_="select2AuthSetting")}
75 ${h.select(node.name, defaults.get(node.name), node.choices, class_="select2AuthSetting")}
71 %elif node.widget == "textarea":
76 %elif node.widget == "textarea":
72 <div class="textarea" style="margin-left: 0px">${h.textarea(node.name, defaults.get(node.name), rows=10)}</div>
77 <div class="textarea" style="margin-left: 0px">${h.textarea(node.name, defaults.get(node.name), rows=10)}</div>
73 %elif node.widget == "readonly":
78 %elif node.widget == "readonly":
74 ${node.default}
79 ${node.default}
75 %else:
80 %else:
76 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
81 This field is of type ${node.typ}, which cannot be displayed. Must be one of [string|int|bool|select].
77 %endif
82 %endif
78
83
79 %if node.name in errors:
84 %if node.name in errors:
80 <span class="error-message">${errors.get(node.name)}</span>
85 <span class="error-message">${errors.get(node.name)}</span>
81 <br />
86 <br />
82 %endif
87 %endif
83 <p class="help-block pre-formatting">${node.description}</p>
88 <p class="help-block pre-formatting">${node.description | n}</p>
84 </div>
89 </div>
85 </div>
90 </div>
86 %endfor
91 %endfor
87
92
88 ## Allow derived templates to add something below the form
93 ## Allow derived templates to add something below the form
89 ## input fields
94 ## input fields
90 %if hasattr(next, 'below_form_fields'):
95 %if hasattr(next, 'below_form_fields'):
91 ${next.below_form_fields()}
96 ${next.below_form_fields()}
92 %endif
97 %endif
93
98
94 <div class="buttons">
99 <div class="buttons">
95 ${h.submit('save',_('Save'),class_="btn")}
100 ${h.submit('save',_('Save'),class_="btn")}
96 </div>
101 </div>
97
102
98 </div>
103 </div>
99 ${h.end_form()}
104 ${h.end_form()}
100 </div>
105 </div>
101 </div>
106 </div>
102
107
103 % if request.GET.get('schema'):
108 % if request.GET.get('schema'):
104 ## this is for development and creation of example configurations for documentation
109 ## this is for development and creation of example configurations for documentation
105 <pre>
110 <pre>
106 % for node in plugin.get_settings_schema():
111 % for node in plugin.get_settings_schema():
107 *option*: `${node.name}` => `${defaults.get(node.name)}`${'\n # '.join(['']+node.description.splitlines())}
112 *option*: `${node.name}` => `${defaults.get(node.name)}`${'\n # '.join(['']+node.description.splitlines())}
108
113
109 % endfor
114 % endfor
110 </pre>
115 </pre>
111
116
112 % endif
117 % endif
113
118
114 </div>
119 </div>
115 </div>
120 </div>
116 </div>
121 </div>
117
122
118 </div>
123 </div>
119 </div>
124 </div>
120
125
121
126
122 <script>
127 <script>
123 $(document).ready(function() {
128 $(document).ready(function() {
124 var select2Options = {
129 var select2Options = {
125 containerCssClass: 'drop-menu',
130 containerCssClass: 'drop-menu',
126 dropdownCssClass: 'drop-menu-dropdown',
131 dropdownCssClass: 'drop-menu-dropdown',
127 dropdownAutoWidth: true,
132 dropdownAutoWidth: true,
128 minimumResultsForSearch: -1
133 minimumResultsForSearch: -1
129 };
134 };
130 $('.select2AuthSetting').select2(select2Options);
135 $('.select2AuthSetting').select2(select2Options);
131
136
132 });
137 });
133 </script>
138 </script>
134 </%def>
139 </%def>
General Comments 0
You need to be logged in to leave comments. Login now