##// END OF EJS Templates
apps: removed utf8 marker
super-admin -
r5053:8da271d0 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,858 +1,858 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23 import operator
23 import operator
24
24
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
26
26
27 from rhodecode.lib import helpers as h, diffs, rc_cache
27 from rhodecode.lib import helpers as h, diffs, rc_cache
28 from rhodecode.lib.utils import repo_name_slug
28 from rhodecode.lib.utils import repo_name_slug
29 from rhodecode.lib.utils2 import (
29 from rhodecode.lib.utils2 import (
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
30 StrictAttributeDict, str2bool, safe_int, datetime_to_time, safe_unicode)
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
31 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
32 from rhodecode.lib.vcs.backends.base import EmptyCommit
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
33 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
34 from rhodecode.model import repo
34 from rhodecode.model import repo
35 from rhodecode.model import repo_group
35 from rhodecode.model import repo_group
36 from rhodecode.model import user_group
36 from rhodecode.model import user_group
37 from rhodecode.model import user
37 from rhodecode.model import user
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39 from rhodecode.model.scm import ScmModel
39 from rhodecode.model.scm import ScmModel
40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
40 from rhodecode.model.settings import VcsSettingsModel, IssueTrackerSettingsModel
41 from rhodecode.model.repo import ReadmeFinder
41 from rhodecode.model.repo import ReadmeFinder
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 ADMIN_PREFIX = '/_admin'
46 ADMIN_PREFIX = '/_admin'
47 STATIC_FILE_PREFIX = '/_static'
47 STATIC_FILE_PREFIX = '/_static'
48
48
49 URL_NAME_REQUIREMENTS = {
49 URL_NAME_REQUIREMENTS = {
50 # group name can have a slash in them, but they must not end with a slash
50 # group name can have a slash in them, but they must not end with a slash
51 'group_name': r'.*?[^/]',
51 'group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
52 'repo_group_name': r'.*?[^/]',
53 # repo names can have a slash in them, but they must not end with a slash
53 # repo names can have a slash in them, but they must not end with a slash
54 'repo_name': r'.*?[^/]',
54 'repo_name': r'.*?[^/]',
55 # file path eats up everything at the end
55 # file path eats up everything at the end
56 'f_path': r'.*',
56 'f_path': r'.*',
57 # reference types
57 # reference types
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
58 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
59 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
60 }
60 }
61
61
62
62
63 def add_route_with_slash(config,name, pattern, **kw):
63 def add_route_with_slash(config,name, pattern, **kw):
64 config.add_route(name, pattern, **kw)
64 config.add_route(name, pattern, **kw)
65 if not pattern.endswith('/'):
65 if not pattern.endswith('/'):
66 config.add_route(name + '_slash', pattern + '/', **kw)
66 config.add_route(name + '_slash', pattern + '/', **kw)
67
67
68
68
69 def add_route_requirements(route_path, requirements=None):
69 def add_route_requirements(route_path, requirements=None):
70 """
70 """
71 Adds regex requirements to pyramid routes using a mapping dict
71 Adds regex requirements to pyramid routes using a mapping dict
72 e.g::
72 e.g::
73 add_route_requirements('{repo_name}/settings')
73 add_route_requirements('{repo_name}/settings')
74 """
74 """
75 requirements = requirements or URL_NAME_REQUIREMENTS
75 requirements = requirements or URL_NAME_REQUIREMENTS
76 for key, regex in requirements.items():
76 for key, regex in requirements.items():
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
77 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
78 return route_path
78 return route_path
79
79
80
80
81 def get_format_ref_id(repo):
81 def get_format_ref_id(repo):
82 """Returns a `repo` specific reference formatter function"""
82 """Returns a `repo` specific reference formatter function"""
83 if h.is_svn(repo):
83 if h.is_svn(repo):
84 return _format_ref_id_svn
84 return _format_ref_id_svn
85 else:
85 else:
86 return _format_ref_id
86 return _format_ref_id
87
87
88
88
89 def _format_ref_id(name, raw_id):
89 def _format_ref_id(name, raw_id):
90 """Default formatting of a given reference `name`"""
90 """Default formatting of a given reference `name`"""
91 return name
91 return name
92
92
93
93
94 def _format_ref_id_svn(name, raw_id):
94 def _format_ref_id_svn(name, raw_id):
95 """Special way of formatting a reference for Subversion including path"""
95 """Special way of formatting a reference for Subversion including path"""
96 return '%s@%s' % (name, raw_id)
96 return '%s@%s' % (name, raw_id)
97
97
98
98
99 class TemplateArgs(StrictAttributeDict):
99 class TemplateArgs(StrictAttributeDict):
100 pass
100 pass
101
101
102
102
103 class BaseAppView(object):
103 class BaseAppView(object):
104
104
105 def __init__(self, context, request):
105 def __init__(self, context, request):
106 self.request = request
106 self.request = request
107 self.context = context
107 self.context = context
108 self.session = request.session
108 self.session = request.session
109 if not hasattr(request, 'user'):
109 if not hasattr(request, 'user'):
110 # NOTE(marcink): edge case, we ended up in matched route
110 # NOTE(marcink): edge case, we ended up in matched route
111 # but probably of web-app context, e.g API CALL/VCS CALL
111 # but probably of web-app context, e.g API CALL/VCS CALL
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
112 if hasattr(request, 'vcs_call') or hasattr(request, 'rpc_method'):
113 log.warning('Unable to process request `%s` in this scope', request)
113 log.warning('Unable to process request `%s` in this scope', request)
114 raise HTTPBadRequest()
114 raise HTTPBadRequest()
115
115
116 self._rhodecode_user = request.user # auth user
116 self._rhodecode_user = request.user # auth user
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
117 self._rhodecode_db_user = self._rhodecode_user.get_instance()
118 self._maybe_needs_password_change(
118 self._maybe_needs_password_change(
119 request.matched_route.name, self._rhodecode_db_user)
119 request.matched_route.name, self._rhodecode_db_user)
120
120
121 def _maybe_needs_password_change(self, view_name, user_obj):
121 def _maybe_needs_password_change(self, view_name, user_obj):
122
122
123 dont_check_views = [
123 dont_check_views = [
124 'channelstream_connect',
124 'channelstream_connect',
125 'ops_ping'
125 'ops_ping'
126 ]
126 ]
127 if view_name in dont_check_views:
127 if view_name in dont_check_views:
128 return
128 return
129
129
130 log.debug('Checking if user %s needs password change on view %s',
130 log.debug('Checking if user %s needs password change on view %s',
131 user_obj, view_name)
131 user_obj, view_name)
132
132
133 skip_user_views = [
133 skip_user_views = [
134 'logout', 'login',
134 'logout', 'login',
135 'my_account_password', 'my_account_password_update'
135 'my_account_password', 'my_account_password_update'
136 ]
136 ]
137
137
138 if not user_obj:
138 if not user_obj:
139 return
139 return
140
140
141 if user_obj.username == User.DEFAULT_USER:
141 if user_obj.username == User.DEFAULT_USER:
142 return
142 return
143
143
144 now = time.time()
144 now = time.time()
145 should_change = user_obj.user_data.get('force_password_change')
145 should_change = user_obj.user_data.get('force_password_change')
146 change_after = safe_int(should_change) or 0
146 change_after = safe_int(should_change) or 0
147 if should_change and now > change_after:
147 if should_change and now > change_after:
148 log.debug('User %s requires password change', user_obj)
148 log.debug('User %s requires password change', user_obj)
149 h.flash('You are required to change your password', 'warning',
149 h.flash('You are required to change your password', 'warning',
150 ignore_duplicate=True)
150 ignore_duplicate=True)
151
151
152 if view_name not in skip_user_views:
152 if view_name not in skip_user_views:
153 raise HTTPFound(
153 raise HTTPFound(
154 self.request.route_path('my_account_password'))
154 self.request.route_path('my_account_password'))
155
155
156 def _log_creation_exception(self, e, repo_name):
156 def _log_creation_exception(self, e, repo_name):
157 _ = self.request.translate
157 _ = self.request.translate
158 reason = None
158 reason = None
159 if len(e.args) == 2:
159 if len(e.args) == 2:
160 reason = e.args[1]
160 reason = e.args[1]
161
161
162 if reason == 'INVALID_CERTIFICATE':
162 if reason == 'INVALID_CERTIFICATE':
163 log.exception(
163 log.exception(
164 'Exception creating a repository: invalid certificate')
164 'Exception creating a repository: invalid certificate')
165 msg = (_('Error creating repository %s: invalid certificate')
165 msg = (_('Error creating repository %s: invalid certificate')
166 % repo_name)
166 % repo_name)
167 else:
167 else:
168 log.exception("Exception creating a repository")
168 log.exception("Exception creating a repository")
169 msg = (_('Error creating repository %s')
169 msg = (_('Error creating repository %s')
170 % repo_name)
170 % repo_name)
171 return msg
171 return msg
172
172
173 def _get_local_tmpl_context(self, include_app_defaults=True):
173 def _get_local_tmpl_context(self, include_app_defaults=True):
174 c = TemplateArgs()
174 c = TemplateArgs()
175 c.auth_user = self.request.user
175 c.auth_user = self.request.user
176 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
176 # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
177 c.rhodecode_user = self.request.user
177 c.rhodecode_user = self.request.user
178
178
179 if include_app_defaults:
179 if include_app_defaults:
180 from rhodecode.lib.base import attach_context_attributes
180 from rhodecode.lib.base import attach_context_attributes
181 attach_context_attributes(c, self.request, self.request.user.user_id)
181 attach_context_attributes(c, self.request, self.request.user.user_id)
182
182
183 c.is_super_admin = c.auth_user.is_admin
183 c.is_super_admin = c.auth_user.is_admin
184
184
185 c.can_create_repo = c.is_super_admin
185 c.can_create_repo = c.is_super_admin
186 c.can_create_repo_group = c.is_super_admin
186 c.can_create_repo_group = c.is_super_admin
187 c.can_create_user_group = c.is_super_admin
187 c.can_create_user_group = c.is_super_admin
188
188
189 c.is_delegated_admin = False
189 c.is_delegated_admin = False
190
190
191 if not c.auth_user.is_default and not c.is_super_admin:
191 if not c.auth_user.is_default and not c.is_super_admin:
192 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
192 c.can_create_repo = h.HasPermissionAny('hg.create.repository')(
193 user=self.request.user)
193 user=self.request.user)
194 repositories = c.auth_user.repositories_admin or c.can_create_repo
194 repositories = c.auth_user.repositories_admin or c.can_create_repo
195
195
196 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
196 c.can_create_repo_group = h.HasPermissionAny('hg.repogroup.create.true')(
197 user=self.request.user)
197 user=self.request.user)
198 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
198 repository_groups = c.auth_user.repository_groups_admin or c.can_create_repo_group
199
199
200 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
200 c.can_create_user_group = h.HasPermissionAny('hg.usergroup.create.true')(
201 user=self.request.user)
201 user=self.request.user)
202 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
202 user_groups = c.auth_user.user_groups_admin or c.can_create_user_group
203 # delegated admin can create, or manage some objects
203 # delegated admin can create, or manage some objects
204 c.is_delegated_admin = repositories or repository_groups or user_groups
204 c.is_delegated_admin = repositories or repository_groups or user_groups
205 return c
205 return c
206
206
207 def _get_template_context(self, tmpl_args, **kwargs):
207 def _get_template_context(self, tmpl_args, **kwargs):
208
208
209 local_tmpl_args = {
209 local_tmpl_args = {
210 'defaults': {},
210 'defaults': {},
211 'errors': {},
211 'errors': {},
212 'c': tmpl_args
212 'c': tmpl_args
213 }
213 }
214 local_tmpl_args.update(kwargs)
214 local_tmpl_args.update(kwargs)
215 return local_tmpl_args
215 return local_tmpl_args
216
216
217 def load_default_context(self):
217 def load_default_context(self):
218 """
218 """
219 example:
219 example:
220
220
221 def load_default_context(self):
221 def load_default_context(self):
222 c = self._get_local_tmpl_context()
222 c = self._get_local_tmpl_context()
223 c.custom_var = 'foobar'
223 c.custom_var = 'foobar'
224
224
225 return c
225 return c
226 """
226 """
227 raise NotImplementedError('Needs implementation in view class')
227 raise NotImplementedError('Needs implementation in view class')
228
228
229
229
230 class RepoAppView(BaseAppView):
230 class RepoAppView(BaseAppView):
231
231
232 def __init__(self, context, request):
232 def __init__(self, context, request):
233 super(RepoAppView, self).__init__(context, request)
233 super(RepoAppView, self).__init__(context, request)
234 self.db_repo = request.db_repo
234 self.db_repo = request.db_repo
235 self.db_repo_name = self.db_repo.repo_name
235 self.db_repo_name = self.db_repo.repo_name
236 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
236 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
237 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
237 self.db_repo_artifacts = ScmModel().get_artifacts(self.db_repo)
238 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
238 self.db_repo_patterns = IssueTrackerSettingsModel(repo=self.db_repo)
239
239
240 def _handle_missing_requirements(self, error):
240 def _handle_missing_requirements(self, error):
241 log.error(
241 log.error(
242 'Requirements are missing for repository %s: %s',
242 'Requirements are missing for repository %s: %s',
243 self.db_repo_name, safe_unicode(error))
243 self.db_repo_name, safe_unicode(error))
244
244
245 def _prepare_and_set_clone_url(self, c):
245 def _prepare_and_set_clone_url(self, c):
246 username = ''
246 username = ''
247 if self._rhodecode_user.username != User.DEFAULT_USER:
247 if self._rhodecode_user.username != User.DEFAULT_USER:
248 username = self._rhodecode_user.username
248 username = self._rhodecode_user.username
249
249
250 _def_clone_uri = c.clone_uri_tmpl
250 _def_clone_uri = c.clone_uri_tmpl
251 _def_clone_uri_id = c.clone_uri_id_tmpl
251 _def_clone_uri_id = c.clone_uri_id_tmpl
252 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
252 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
253
253
254 c.clone_repo_url = self.db_repo.clone_url(
254 c.clone_repo_url = self.db_repo.clone_url(
255 user=username, uri_tmpl=_def_clone_uri)
255 user=username, uri_tmpl=_def_clone_uri)
256 c.clone_repo_url_id = self.db_repo.clone_url(
256 c.clone_repo_url_id = self.db_repo.clone_url(
257 user=username, uri_tmpl=_def_clone_uri_id)
257 user=username, uri_tmpl=_def_clone_uri_id)
258 c.clone_repo_url_ssh = self.db_repo.clone_url(
258 c.clone_repo_url_ssh = self.db_repo.clone_url(
259 uri_tmpl=_def_clone_uri_ssh, ssh=True)
259 uri_tmpl=_def_clone_uri_ssh, ssh=True)
260
260
261 def _get_local_tmpl_context(self, include_app_defaults=True):
261 def _get_local_tmpl_context(self, include_app_defaults=True):
262 _ = self.request.translate
262 _ = self.request.translate
263 c = super(RepoAppView, self)._get_local_tmpl_context(
263 c = super(RepoAppView, self)._get_local_tmpl_context(
264 include_app_defaults=include_app_defaults)
264 include_app_defaults=include_app_defaults)
265
265
266 # register common vars for this type of view
266 # register common vars for this type of view
267 c.rhodecode_db_repo = self.db_repo
267 c.rhodecode_db_repo = self.db_repo
268 c.repo_name = self.db_repo_name
268 c.repo_name = self.db_repo_name
269 c.repository_pull_requests = self.db_repo_pull_requests
269 c.repository_pull_requests = self.db_repo_pull_requests
270 c.repository_artifacts = self.db_repo_artifacts
270 c.repository_artifacts = self.db_repo_artifacts
271 c.repository_is_user_following = ScmModel().is_following_repo(
271 c.repository_is_user_following = ScmModel().is_following_repo(
272 self.db_repo_name, self._rhodecode_user.user_id)
272 self.db_repo_name, self._rhodecode_user.user_id)
273 self.path_filter = PathFilter(None)
273 self.path_filter = PathFilter(None)
274
274
275 c.repository_requirements_missing = {}
275 c.repository_requirements_missing = {}
276 try:
276 try:
277 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
277 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
278 # NOTE(marcink):
278 # NOTE(marcink):
279 # comparison to None since if it's an object __bool__ is expensive to
279 # comparison to None since if it's an object __bool__ is expensive to
280 # calculate
280 # calculate
281 if self.rhodecode_vcs_repo is not None:
281 if self.rhodecode_vcs_repo is not None:
282 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
282 path_perms = self.rhodecode_vcs_repo.get_path_permissions(
283 c.auth_user.username)
283 c.auth_user.username)
284 self.path_filter = PathFilter(path_perms)
284 self.path_filter = PathFilter(path_perms)
285 except RepositoryRequirementError as e:
285 except RepositoryRequirementError as e:
286 c.repository_requirements_missing = {'error': str(e)}
286 c.repository_requirements_missing = {'error': str(e)}
287 self._handle_missing_requirements(e)
287 self._handle_missing_requirements(e)
288 self.rhodecode_vcs_repo = None
288 self.rhodecode_vcs_repo = None
289
289
290 c.path_filter = self.path_filter # used by atom_feed_entry.mako
290 c.path_filter = self.path_filter # used by atom_feed_entry.mako
291
291
292 if self.rhodecode_vcs_repo is None:
292 if self.rhodecode_vcs_repo is None:
293 # unable to fetch this repo as vcs instance, report back to user
293 # unable to fetch this repo as vcs instance, report back to user
294 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
294 log.debug('Repository was not found on filesystem, check if it exists or is not damaged')
295 h.flash(_(
295 h.flash(_(
296 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
296 "The repository `%(repo_name)s` cannot be loaded in filesystem. "
297 "Please check if it exist, or is not damaged.") %
297 "Please check if it exist, or is not damaged.") %
298 {'repo_name': c.repo_name},
298 {'repo_name': c.repo_name},
299 category='error', ignore_duplicate=True)
299 category='error', ignore_duplicate=True)
300 if c.repository_requirements_missing:
300 if c.repository_requirements_missing:
301 route = self.request.matched_route.name
301 route = self.request.matched_route.name
302 if route.startswith(('edit_repo', 'repo_summary')):
302 if route.startswith(('edit_repo', 'repo_summary')):
303 # allow summary and edit repo on missing requirements
303 # allow summary and edit repo on missing requirements
304 return c
304 return c
305
305
306 raise HTTPFound(
306 raise HTTPFound(
307 h.route_path('repo_summary', repo_name=self.db_repo_name))
307 h.route_path('repo_summary', repo_name=self.db_repo_name))
308
308
309 else: # redirect if we don't show missing requirements
309 else: # redirect if we don't show missing requirements
310 raise HTTPFound(h.route_path('home'))
310 raise HTTPFound(h.route_path('home'))
311
311
312 c.has_origin_repo_read_perm = False
312 c.has_origin_repo_read_perm = False
313 if self.db_repo.fork:
313 if self.db_repo.fork:
314 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
314 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
315 'repository.write', 'repository.read', 'repository.admin')(
315 'repository.write', 'repository.read', 'repository.admin')(
316 self.db_repo.fork.repo_name, 'summary fork link')
316 self.db_repo.fork.repo_name, 'summary fork link')
317
317
318 return c
318 return c
319
319
320 def _get_f_path_unchecked(self, matchdict, default=None):
320 def _get_f_path_unchecked(self, matchdict, default=None):
321 """
321 """
322 Should only be used by redirects, everything else should call _get_f_path
322 Should only be used by redirects, everything else should call _get_f_path
323 """
323 """
324 f_path = matchdict.get('f_path')
324 f_path = matchdict.get('f_path')
325 if f_path:
325 if f_path:
326 # fix for multiple initial slashes that causes errors for GIT
326 # fix for multiple initial slashes that causes errors for GIT
327 return f_path.lstrip('/')
327 return f_path.lstrip('/')
328
328
329 return default
329 return default
330
330
331 def _get_f_path(self, matchdict, default=None):
331 def _get_f_path(self, matchdict, default=None):
332 f_path_match = self._get_f_path_unchecked(matchdict, default)
332 f_path_match = self._get_f_path_unchecked(matchdict, default)
333 return self.path_filter.assert_path_permissions(f_path_match)
333 return self.path_filter.assert_path_permissions(f_path_match)
334
334
335 def _get_general_setting(self, target_repo, settings_key, default=False):
335 def _get_general_setting(self, target_repo, settings_key, default=False):
336 settings_model = VcsSettingsModel(repo=target_repo)
336 settings_model = VcsSettingsModel(repo=target_repo)
337 settings = settings_model.get_general_settings()
337 settings = settings_model.get_general_settings()
338 return settings.get(settings_key, default)
338 return settings.get(settings_key, default)
339
339
340 def _get_repo_setting(self, target_repo, settings_key, default=False):
340 def _get_repo_setting(self, target_repo, settings_key, default=False):
341 settings_model = VcsSettingsModel(repo=target_repo)
341 settings_model = VcsSettingsModel(repo=target_repo)
342 settings = settings_model.get_repo_settings_inherited()
342 settings = settings_model.get_repo_settings_inherited()
343 return settings.get(settings_key, default)
343 return settings.get(settings_key, default)
344
344
345 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
345 def _get_readme_data(self, db_repo, renderer_type, commit_id=None, path='/'):
346 log.debug('Looking for README file at path %s', path)
346 log.debug('Looking for README file at path %s', path)
347 if commit_id:
347 if commit_id:
348 landing_commit_id = commit_id
348 landing_commit_id = commit_id
349 else:
349 else:
350 landing_commit = db_repo.get_landing_commit()
350 landing_commit = db_repo.get_landing_commit()
351 if isinstance(landing_commit, EmptyCommit):
351 if isinstance(landing_commit, EmptyCommit):
352 return None, None
352 return None, None
353 landing_commit_id = landing_commit.raw_id
353 landing_commit_id = landing_commit.raw_id
354
354
355 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
355 cache_namespace_uid = 'cache_repo.{}'.format(db_repo.repo_id)
356 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
356 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
357 start = time.time()
357 start = time.time()
358
358
359 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
359 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
360 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
360 def generate_repo_readme(repo_id, _commit_id, _repo_name, _readme_search_path, _renderer_type):
361 readme_data = None
361 readme_data = None
362 readme_filename = None
362 readme_filename = None
363
363
364 commit = db_repo.get_commit(_commit_id)
364 commit = db_repo.get_commit(_commit_id)
365 log.debug("Searching for a README file at commit %s.", _commit_id)
365 log.debug("Searching for a README file at commit %s.", _commit_id)
366 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
366 readme_node = ReadmeFinder(_renderer_type).search(commit, path=_readme_search_path)
367
367
368 if readme_node:
368 if readme_node:
369 log.debug('Found README node: %s', readme_node)
369 log.debug('Found README node: %s', readme_node)
370 relative_urls = {
370 relative_urls = {
371 'raw': h.route_path(
371 'raw': h.route_path(
372 'repo_file_raw', repo_name=_repo_name,
372 'repo_file_raw', repo_name=_repo_name,
373 commit_id=commit.raw_id, f_path=readme_node.path),
373 commit_id=commit.raw_id, f_path=readme_node.path),
374 'standard': h.route_path(
374 'standard': h.route_path(
375 'repo_files', repo_name=_repo_name,
375 'repo_files', repo_name=_repo_name,
376 commit_id=commit.raw_id, f_path=readme_node.path),
376 commit_id=commit.raw_id, f_path=readme_node.path),
377 }
377 }
378
378
379 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
379 readme_data = self._render_readme_or_none(commit, readme_node, relative_urls)
380 readme_filename = readme_node.unicode_path
380 readme_filename = readme_node.unicode_path
381
381
382 return readme_data, readme_filename
382 return readme_data, readme_filename
383
383
384 readme_data, readme_filename = generate_repo_readme(
384 readme_data, readme_filename = generate_repo_readme(
385 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
385 db_repo.repo_id, landing_commit_id, db_repo.repo_name, path, renderer_type,)
386
386
387 compute_time = time.time() - start
387 compute_time = time.time() - start
388 log.debug('Repo README for path %s generated and computed in %.4fs',
388 log.debug('Repo README for path %s generated and computed in %.4fs',
389 path, compute_time)
389 path, compute_time)
390 return readme_data, readme_filename
390 return readme_data, readme_filename
391
391
392 def _render_readme_or_none(self, commit, readme_node, relative_urls):
392 def _render_readme_or_none(self, commit, readme_node, relative_urls):
393 log.debug('Found README file `%s` rendering...', readme_node.path)
393 log.debug('Found README file `%s` rendering...', readme_node.path)
394 renderer = MarkupRenderer()
394 renderer = MarkupRenderer()
395 try:
395 try:
396 html_source = renderer.render(
396 html_source = renderer.render(
397 readme_node.content, filename=readme_node.path)
397 readme_node.content, filename=readme_node.path)
398 if relative_urls:
398 if relative_urls:
399 return relative_links(html_source, relative_urls)
399 return relative_links(html_source, relative_urls)
400 return html_source
400 return html_source
401 except Exception:
401 except Exception:
402 log.exception(
402 log.exception(
403 "Exception while trying to render the README")
403 "Exception while trying to render the README")
404
404
405 def get_recache_flag(self):
405 def get_recache_flag(self):
406 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
406 for flag_name in ['force_recache', 'force-recache', 'no-cache']:
407 flag_val = self.request.GET.get(flag_name)
407 flag_val = self.request.GET.get(flag_name)
408 if str2bool(flag_val):
408 if str2bool(flag_val):
409 return True
409 return True
410 return False
410 return False
411
411
412 def get_commit_preload_attrs(cls):
412 def get_commit_preload_attrs(cls):
413 pre_load = ['author', 'branch', 'date', 'message', 'parents',
413 pre_load = ['author', 'branch', 'date', 'message', 'parents',
414 'obsolete', 'phase', 'hidden']
414 'obsolete', 'phase', 'hidden']
415 return pre_load
415 return pre_load
416
416
417
417
418 class PathFilter(object):
418 class PathFilter(object):
419
419
420 # Expects and instance of BasePathPermissionChecker or None
420 # Expects and instance of BasePathPermissionChecker or None
421 def __init__(self, permission_checker):
421 def __init__(self, permission_checker):
422 self.permission_checker = permission_checker
422 self.permission_checker = permission_checker
423
423
424 def assert_path_permissions(self, path):
424 def assert_path_permissions(self, path):
425 if self.path_access_allowed(path):
425 if self.path_access_allowed(path):
426 return path
426 return path
427 raise HTTPForbidden()
427 raise HTTPForbidden()
428
428
429 def path_access_allowed(self, path):
429 def path_access_allowed(self, path):
430 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
430 log.debug('Checking ACL permissions for PathFilter for `%s`', path)
431 if self.permission_checker:
431 if self.permission_checker:
432 has_access = path and self.permission_checker.has_access(path)
432 has_access = path and self.permission_checker.has_access(path)
433 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
433 log.debug('ACL Permissions checker enabled, ACL Check has_access: %s', has_access)
434 return has_access
434 return has_access
435
435
436 log.debug('ACL permissions checker not enabled, skipping...')
436 log.debug('ACL permissions checker not enabled, skipping...')
437 return True
437 return True
438
438
439 def filter_patchset(self, patchset):
439 def filter_patchset(self, patchset):
440 if not self.permission_checker or not patchset:
440 if not self.permission_checker or not patchset:
441 return patchset, False
441 return patchset, False
442 had_filtered = False
442 had_filtered = False
443 filtered_patchset = []
443 filtered_patchset = []
444 for patch in patchset:
444 for patch in patchset:
445 filename = patch.get('filename', None)
445 filename = patch.get('filename', None)
446 if not filename or self.permission_checker.has_access(filename):
446 if not filename or self.permission_checker.has_access(filename):
447 filtered_patchset.append(patch)
447 filtered_patchset.append(patch)
448 else:
448 else:
449 had_filtered = True
449 had_filtered = True
450 if had_filtered:
450 if had_filtered:
451 if isinstance(patchset, diffs.LimitedDiffContainer):
451 if isinstance(patchset, diffs.LimitedDiffContainer):
452 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
452 filtered_patchset = diffs.LimitedDiffContainer(patchset.diff_limit, patchset.cur_diff_size, filtered_patchset)
453 return filtered_patchset, True
453 return filtered_patchset, True
454 else:
454 else:
455 return patchset, False
455 return patchset, False
456
456
457 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
457 def render_patchset_filtered(self, diffset, patchset, source_ref=None, target_ref=None):
458 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
458 filtered_patchset, has_hidden_changes = self.filter_patchset(patchset)
459 result = diffset.render_patchset(
459 result = diffset.render_patchset(
460 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
460 filtered_patchset, source_ref=source_ref, target_ref=target_ref)
461 result.has_hidden_changes = has_hidden_changes
461 result.has_hidden_changes = has_hidden_changes
462 return result
462 return result
463
463
464 def get_raw_patch(self, diff_processor):
464 def get_raw_patch(self, diff_processor):
465 if self.permission_checker is None:
465 if self.permission_checker is None:
466 return diff_processor.as_raw()
466 return diff_processor.as_raw()
467 elif self.permission_checker.has_full_access:
467 elif self.permission_checker.has_full_access:
468 return diff_processor.as_raw()
468 return diff_processor.as_raw()
469 else:
469 else:
470 return '# Repository has user-specific filters, raw patch generation is disabled.'
470 return '# Repository has user-specific filters, raw patch generation is disabled.'
471
471
472 @property
472 @property
473 def is_enabled(self):
473 def is_enabled(self):
474 return self.permission_checker is not None
474 return self.permission_checker is not None
475
475
476
476
477 class RepoGroupAppView(BaseAppView):
477 class RepoGroupAppView(BaseAppView):
478 def __init__(self, context, request):
478 def __init__(self, context, request):
479 super(RepoGroupAppView, self).__init__(context, request)
479 super(RepoGroupAppView, self).__init__(context, request)
480 self.db_repo_group = request.db_repo_group
480 self.db_repo_group = request.db_repo_group
481 self.db_repo_group_name = self.db_repo_group.group_name
481 self.db_repo_group_name = self.db_repo_group.group_name
482
482
483 def _get_local_tmpl_context(self, include_app_defaults=True):
483 def _get_local_tmpl_context(self, include_app_defaults=True):
484 _ = self.request.translate
484 _ = self.request.translate
485 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
485 c = super(RepoGroupAppView, self)._get_local_tmpl_context(
486 include_app_defaults=include_app_defaults)
486 include_app_defaults=include_app_defaults)
487 c.repo_group = self.db_repo_group
487 c.repo_group = self.db_repo_group
488 return c
488 return c
489
489
490 def _revoke_perms_on_yourself(self, form_result):
490 def _revoke_perms_on_yourself(self, form_result):
491 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
491 _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
492 form_result['perm_updates'])
492 form_result['perm_updates'])
493 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
493 _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
494 form_result['perm_additions'])
494 form_result['perm_additions'])
495 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
495 _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
496 form_result['perm_deletions'])
496 form_result['perm_deletions'])
497 admin_perm = 'group.admin'
497 admin_perm = 'group.admin'
498 if _updates and _updates[0][1] != admin_perm or \
498 if _updates and _updates[0][1] != admin_perm or \
499 _additions and _additions[0][1] != admin_perm or \
499 _additions and _additions[0][1] != admin_perm or \
500 _deletions and _deletions[0][1] != admin_perm:
500 _deletions and _deletions[0][1] != admin_perm:
501 return True
501 return True
502 return False
502 return False
503
503
504
504
505 class UserGroupAppView(BaseAppView):
505 class UserGroupAppView(BaseAppView):
506 def __init__(self, context, request):
506 def __init__(self, context, request):
507 super(UserGroupAppView, self).__init__(context, request)
507 super(UserGroupAppView, self).__init__(context, request)
508 self.db_user_group = request.db_user_group
508 self.db_user_group = request.db_user_group
509 self.db_user_group_name = self.db_user_group.users_group_name
509 self.db_user_group_name = self.db_user_group.users_group_name
510
510
511
511
512 class UserAppView(BaseAppView):
512 class UserAppView(BaseAppView):
513 def __init__(self, context, request):
513 def __init__(self, context, request):
514 super(UserAppView, self).__init__(context, request)
514 super(UserAppView, self).__init__(context, request)
515 self.db_user = request.db_user
515 self.db_user = request.db_user
516 self.db_user_id = self.db_user.user_id
516 self.db_user_id = self.db_user.user_id
517
517
518 _ = self.request.translate
518 _ = self.request.translate
519 if not request.db_user_supports_default:
519 if not request.db_user_supports_default:
520 if self.db_user.username == User.DEFAULT_USER:
520 if self.db_user.username == User.DEFAULT_USER:
521 h.flash(_("Editing user `{}` is disabled.".format(
521 h.flash(_("Editing user `{}` is disabled.".format(
522 User.DEFAULT_USER)), category='warning')
522 User.DEFAULT_USER)), category='warning')
523 raise HTTPFound(h.route_path('users'))
523 raise HTTPFound(h.route_path('users'))
524
524
525
525
526 class DataGridAppView(object):
526 class DataGridAppView(object):
527 """
527 """
528 Common class to have re-usable grid rendering components
528 Common class to have re-usable grid rendering components
529 """
529 """
530
530
531 def _extract_ordering(self, request, column_map=None):
531 def _extract_ordering(self, request, column_map=None):
532 column_map = column_map or {}
532 column_map = column_map or {}
533 column_index = safe_int(request.GET.get('order[0][column]'))
533 column_index = safe_int(request.GET.get('order[0][column]'))
534 order_dir = request.GET.get(
534 order_dir = request.GET.get(
535 'order[0][dir]', 'desc')
535 'order[0][dir]', 'desc')
536 order_by = request.GET.get(
536 order_by = request.GET.get(
537 'columns[%s][data][sort]' % column_index, 'name_raw')
537 'columns[%s][data][sort]' % column_index, 'name_raw')
538
538
539 # translate datatable to DB columns
539 # translate datatable to DB columns
540 order_by = column_map.get(order_by) or order_by
540 order_by = column_map.get(order_by) or order_by
541
541
542 search_q = request.GET.get('search[value]')
542 search_q = request.GET.get('search[value]')
543 return search_q, order_by, order_dir
543 return search_q, order_by, order_dir
544
544
545 def _extract_chunk(self, request):
545 def _extract_chunk(self, request):
546 start = safe_int(request.GET.get('start'), 0)
546 start = safe_int(request.GET.get('start'), 0)
547 length = safe_int(request.GET.get('length'), 25)
547 length = safe_int(request.GET.get('length'), 25)
548 draw = safe_int(request.GET.get('draw'))
548 draw = safe_int(request.GET.get('draw'))
549 return draw, start, length
549 return draw, start, length
550
550
551 def _get_order_col(self, order_by, model):
551 def _get_order_col(self, order_by, model):
552 if isinstance(order_by, str):
552 if isinstance(order_by, str):
553 try:
553 try:
554 return operator.attrgetter(order_by)(model)
554 return operator.attrgetter(order_by)(model)
555 except AttributeError:
555 except AttributeError:
556 return None
556 return None
557 else:
557 else:
558 return order_by
558 return order_by
559
559
560
560
561 class BaseReferencesView(RepoAppView):
561 class BaseReferencesView(RepoAppView):
562 """
562 """
563 Base for reference view for branches, tags and bookmarks.
563 Base for reference view for branches, tags and bookmarks.
564 """
564 """
565 def load_default_context(self):
565 def load_default_context(self):
566 c = self._get_local_tmpl_context()
566 c = self._get_local_tmpl_context()
567 return c
567 return c
568
568
569 def load_refs_context(self, ref_items, partials_template):
569 def load_refs_context(self, ref_items, partials_template):
570 _render = self.request.get_partial_renderer(partials_template)
570 _render = self.request.get_partial_renderer(partials_template)
571 pre_load = ["author", "date", "message", "parents"]
571 pre_load = ["author", "date", "message", "parents"]
572
572
573 is_svn = h.is_svn(self.rhodecode_vcs_repo)
573 is_svn = h.is_svn(self.rhodecode_vcs_repo)
574 is_hg = h.is_hg(self.rhodecode_vcs_repo)
574 is_hg = h.is_hg(self.rhodecode_vcs_repo)
575
575
576 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
576 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
577
577
578 closed_refs = {}
578 closed_refs = {}
579 if is_hg:
579 if is_hg:
580 closed_refs = self.rhodecode_vcs_repo.branches_closed
580 closed_refs = self.rhodecode_vcs_repo.branches_closed
581
581
582 data = []
582 data = []
583 for ref_name, commit_id in ref_items:
583 for ref_name, commit_id in ref_items:
584 commit = self.rhodecode_vcs_repo.get_commit(
584 commit = self.rhodecode_vcs_repo.get_commit(
585 commit_id=commit_id, pre_load=pre_load)
585 commit_id=commit_id, pre_load=pre_load)
586 closed = ref_name in closed_refs
586 closed = ref_name in closed_refs
587
587
588 # TODO: johbo: Unify generation of reference links
588 # TODO: johbo: Unify generation of reference links
589 use_commit_id = '/' in ref_name or is_svn
589 use_commit_id = '/' in ref_name or is_svn
590
590
591 if use_commit_id:
591 if use_commit_id:
592 files_url = h.route_path(
592 files_url = h.route_path(
593 'repo_files',
593 'repo_files',
594 repo_name=self.db_repo_name,
594 repo_name=self.db_repo_name,
595 f_path=ref_name if is_svn else '',
595 f_path=ref_name if is_svn else '',
596 commit_id=commit_id,
596 commit_id=commit_id,
597 _query=dict(at=ref_name)
597 _query=dict(at=ref_name)
598 )
598 )
599
599
600 else:
600 else:
601 files_url = h.route_path(
601 files_url = h.route_path(
602 'repo_files',
602 'repo_files',
603 repo_name=self.db_repo_name,
603 repo_name=self.db_repo_name,
604 f_path=ref_name if is_svn else '',
604 f_path=ref_name if is_svn else '',
605 commit_id=ref_name,
605 commit_id=ref_name,
606 _query=dict(at=ref_name)
606 _query=dict(at=ref_name)
607 )
607 )
608
608
609 data.append({
609 data.append({
610 "name": _render('name', ref_name, files_url, closed),
610 "name": _render('name', ref_name, files_url, closed),
611 "name_raw": ref_name,
611 "name_raw": ref_name,
612 "date": _render('date', commit.date),
612 "date": _render('date', commit.date),
613 "date_raw": datetime_to_time(commit.date),
613 "date_raw": datetime_to_time(commit.date),
614 "author": _render('author', commit.author),
614 "author": _render('author', commit.author),
615 "commit": _render(
615 "commit": _render(
616 'commit', commit.message, commit.raw_id, commit.idx),
616 'commit', commit.message, commit.raw_id, commit.idx),
617 "commit_raw": commit.idx,
617 "commit_raw": commit.idx,
618 "compare": _render(
618 "compare": _render(
619 'compare', format_ref_id(ref_name, commit.raw_id)),
619 'compare', format_ref_id(ref_name, commit.raw_id)),
620 })
620 })
621
621
622 return data
622 return data
623
623
624
624
625 class RepoRoutePredicate(object):
625 class RepoRoutePredicate(object):
626 def __init__(self, val, config):
626 def __init__(self, val, config):
627 self.val = val
627 self.val = val
628
628
629 def text(self):
629 def text(self):
630 return 'repo_route = %s' % self.val
630 return 'repo_route = %s' % self.val
631
631
632 phash = text
632 phash = text
633
633
634 def __call__(self, info, request):
634 def __call__(self, info, request):
635 if hasattr(request, 'vcs_call'):
635 if hasattr(request, 'vcs_call'):
636 # skip vcs calls
636 # skip vcs calls
637 return
637 return
638
638
639 repo_name = info['match']['repo_name']
639 repo_name = info['match']['repo_name']
640
640
641 repo_name_parts = repo_name.split('/')
641 repo_name_parts = repo_name.split('/')
642 repo_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_name_parts)]
642 repo_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_name_parts)]
643
643
644 if repo_name_parts != repo_slugs:
644 if repo_name_parts != repo_slugs:
645 # short-skip if the repo-name doesn't follow slug rule
645 # short-skip if the repo-name doesn't follow slug rule
646 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
646 log.warning('repo_name: %s is different than slug %s', repo_name_parts, repo_slugs)
647 return False
647 return False
648
648
649 repo_model = repo.RepoModel()
649 repo_model = repo.RepoModel()
650
650
651 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
651 by_name_match = repo_model.get_by_repo_name(repo_name, cache=False)
652
652
653 def redirect_if_creating(route_info, db_repo):
653 def redirect_if_creating(route_info, db_repo):
654 skip_views = ['edit_repo_advanced_delete']
654 skip_views = ['edit_repo_advanced_delete']
655 route = route_info['route']
655 route = route_info['route']
656 # we should skip delete view so we can actually "remove" repositories
656 # we should skip delete view so we can actually "remove" repositories
657 # if they get stuck in creating state.
657 # if they get stuck in creating state.
658 if route.name in skip_views:
658 if route.name in skip_views:
659 return
659 return
660
660
661 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
661 if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
662 repo_creating_url = request.route_path(
662 repo_creating_url = request.route_path(
663 'repo_creating', repo_name=db_repo.repo_name)
663 'repo_creating', repo_name=db_repo.repo_name)
664 raise HTTPFound(repo_creating_url)
664 raise HTTPFound(repo_creating_url)
665
665
666 if by_name_match:
666 if by_name_match:
667 # register this as request object we can re-use later
667 # register this as request object we can re-use later
668 request.db_repo = by_name_match
668 request.db_repo = by_name_match
669 redirect_if_creating(info, by_name_match)
669 redirect_if_creating(info, by_name_match)
670 return True
670 return True
671
671
672 by_id_match = repo_model.get_repo_by_id(repo_name)
672 by_id_match = repo_model.get_repo_by_id(repo_name)
673 if by_id_match:
673 if by_id_match:
674 request.db_repo = by_id_match
674 request.db_repo = by_id_match
675 redirect_if_creating(info, by_id_match)
675 redirect_if_creating(info, by_id_match)
676 return True
676 return True
677
677
678 return False
678 return False
679
679
680
680
681 class RepoForbidArchivedRoutePredicate(object):
681 class RepoForbidArchivedRoutePredicate(object):
682 def __init__(self, val, config):
682 def __init__(self, val, config):
683 self.val = val
683 self.val = val
684
684
685 def text(self):
685 def text(self):
686 return 'repo_forbid_archived = %s' % self.val
686 return 'repo_forbid_archived = %s' % self.val
687
687
688 phash = text
688 phash = text
689
689
690 def __call__(self, info, request):
690 def __call__(self, info, request):
691 _ = request.translate
691 _ = request.translate
692 rhodecode_db_repo = request.db_repo
692 rhodecode_db_repo = request.db_repo
693
693
694 log.debug(
694 log.debug(
695 '%s checking if archived flag for repo for %s',
695 '%s checking if archived flag for repo for %s',
696 self.__class__.__name__, rhodecode_db_repo.repo_name)
696 self.__class__.__name__, rhodecode_db_repo.repo_name)
697
697
698 if rhodecode_db_repo.archived:
698 if rhodecode_db_repo.archived:
699 log.warning('Current view is not supported for archived repo:%s',
699 log.warning('Current view is not supported for archived repo:%s',
700 rhodecode_db_repo.repo_name)
700 rhodecode_db_repo.repo_name)
701
701
702 h.flash(
702 h.flash(
703 h.literal(_('Action not supported for archived repository.')),
703 h.literal(_('Action not supported for archived repository.')),
704 category='warning')
704 category='warning')
705 summary_url = request.route_path(
705 summary_url = request.route_path(
706 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
706 'repo_summary', repo_name=rhodecode_db_repo.repo_name)
707 raise HTTPFound(summary_url)
707 raise HTTPFound(summary_url)
708 return True
708 return True
709
709
710
710
711 class RepoTypeRoutePredicate(object):
711 class RepoTypeRoutePredicate(object):
712 def __init__(self, val, config):
712 def __init__(self, val, config):
713 self.val = val or ['hg', 'git', 'svn']
713 self.val = val or ['hg', 'git', 'svn']
714
714
715 def text(self):
715 def text(self):
716 return 'repo_accepted_type = %s' % self.val
716 return 'repo_accepted_type = %s' % self.val
717
717
718 phash = text
718 phash = text
719
719
720 def __call__(self, info, request):
720 def __call__(self, info, request):
721 if hasattr(request, 'vcs_call'):
721 if hasattr(request, 'vcs_call'):
722 # skip vcs calls
722 # skip vcs calls
723 return
723 return
724
724
725 rhodecode_db_repo = request.db_repo
725 rhodecode_db_repo = request.db_repo
726
726
727 log.debug(
727 log.debug(
728 '%s checking repo type for %s in %s',
728 '%s checking repo type for %s in %s',
729 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
729 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
730
730
731 if rhodecode_db_repo.repo_type in self.val:
731 if rhodecode_db_repo.repo_type in self.val:
732 return True
732 return True
733 else:
733 else:
734 log.warning('Current view is not supported for repo type:%s',
734 log.warning('Current view is not supported for repo type:%s',
735 rhodecode_db_repo.repo_type)
735 rhodecode_db_repo.repo_type)
736 return False
736 return False
737
737
738
738
739 class RepoGroupRoutePredicate(object):
739 class RepoGroupRoutePredicate(object):
740 def __init__(self, val, config):
740 def __init__(self, val, config):
741 self.val = val
741 self.val = val
742
742
743 def text(self):
743 def text(self):
744 return 'repo_group_route = %s' % self.val
744 return 'repo_group_route = %s' % self.val
745
745
746 phash = text
746 phash = text
747
747
748 def __call__(self, info, request):
748 def __call__(self, info, request):
749 if hasattr(request, 'vcs_call'):
749 if hasattr(request, 'vcs_call'):
750 # skip vcs calls
750 # skip vcs calls
751 return
751 return
752
752
753 repo_group_name = info['match']['repo_group_name']
753 repo_group_name = info['match']['repo_group_name']
754
754
755 repo_group_name_parts = repo_group_name.split('/')
755 repo_group_name_parts = repo_group_name.split('/')
756 repo_group_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_group_name_parts)]
756 repo_group_slugs = [x for x in map(lambda x: repo_name_slug(x), repo_group_name_parts)]
757 if repo_group_name_parts != repo_group_slugs:
757 if repo_group_name_parts != repo_group_slugs:
758 # short-skip if the repo-name doesn't follow slug rule
758 # short-skip if the repo-name doesn't follow slug rule
759 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
759 log.warning('repo_group_name: %s is different than slug %s', repo_group_name_parts, repo_group_slugs)
760 return False
760 return False
761
761
762 repo_group_model = repo_group.RepoGroupModel()
762 repo_group_model = repo_group.RepoGroupModel()
763 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
763 by_name_match = repo_group_model.get_by_group_name(repo_group_name, cache=False)
764
764
765 if by_name_match:
765 if by_name_match:
766 # register this as request object we can re-use later
766 # register this as request object we can re-use later
767 request.db_repo_group = by_name_match
767 request.db_repo_group = by_name_match
768 return True
768 return True
769
769
770 return False
770 return False
771
771
772
772
773 class UserGroupRoutePredicate(object):
773 class UserGroupRoutePredicate(object):
774 def __init__(self, val, config):
774 def __init__(self, val, config):
775 self.val = val
775 self.val = val
776
776
777 def text(self):
777 def text(self):
778 return 'user_group_route = %s' % self.val
778 return 'user_group_route = %s' % self.val
779
779
780 phash = text
780 phash = text
781
781
782 def __call__(self, info, request):
782 def __call__(self, info, request):
783 if hasattr(request, 'vcs_call'):
783 if hasattr(request, 'vcs_call'):
784 # skip vcs calls
784 # skip vcs calls
785 return
785 return
786
786
787 user_group_id = info['match']['user_group_id']
787 user_group_id = info['match']['user_group_id']
788 user_group_model = user_group.UserGroup()
788 user_group_model = user_group.UserGroup()
789 by_id_match = user_group_model.get(user_group_id, cache=False)
789 by_id_match = user_group_model.get(user_group_id, cache=False)
790
790
791 if by_id_match:
791 if by_id_match:
792 # register this as request object we can re-use later
792 # register this as request object we can re-use later
793 request.db_user_group = by_id_match
793 request.db_user_group = by_id_match
794 return True
794 return True
795
795
796 return False
796 return False
797
797
798
798
799 class UserRoutePredicateBase(object):
799 class UserRoutePredicateBase(object):
800 supports_default = None
800 supports_default = None
801
801
802 def __init__(self, val, config):
802 def __init__(self, val, config):
803 self.val = val
803 self.val = val
804
804
805 def text(self):
805 def text(self):
806 raise NotImplementedError()
806 raise NotImplementedError()
807
807
808 def __call__(self, info, request):
808 def __call__(self, info, request):
809 if hasattr(request, 'vcs_call'):
809 if hasattr(request, 'vcs_call'):
810 # skip vcs calls
810 # skip vcs calls
811 return
811 return
812
812
813 user_id = info['match']['user_id']
813 user_id = info['match']['user_id']
814 user_model = user.User()
814 user_model = user.User()
815 by_id_match = user_model.get(user_id, cache=False)
815 by_id_match = user_model.get(user_id, cache=False)
816
816
817 if by_id_match:
817 if by_id_match:
818 # register this as request object we can re-use later
818 # register this as request object we can re-use later
819 request.db_user = by_id_match
819 request.db_user = by_id_match
820 request.db_user_supports_default = self.supports_default
820 request.db_user_supports_default = self.supports_default
821 return True
821 return True
822
822
823 return False
823 return False
824
824
825
825
826 class UserRoutePredicate(UserRoutePredicateBase):
826 class UserRoutePredicate(UserRoutePredicateBase):
827 supports_default = False
827 supports_default = False
828
828
829 def text(self):
829 def text(self):
830 return 'user_route = %s' % self.val
830 return 'user_route = %s' % self.val
831
831
832 phash = text
832 phash = text
833
833
834
834
835 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
835 class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
836 supports_default = True
836 supports_default = True
837
837
838 def text(self):
838 def text(self):
839 return 'user_with_default_route = %s' % self.val
839 return 'user_with_default_route = %s' % self.val
840
840
841 phash = text
841 phash = text
842
842
843
843
844 def includeme(config):
844 def includeme(config):
845 config.add_route_predicate(
845 config.add_route_predicate(
846 'repo_route', RepoRoutePredicate)
846 'repo_route', RepoRoutePredicate)
847 config.add_route_predicate(
847 config.add_route_predicate(
848 'repo_accepted_types', RepoTypeRoutePredicate)
848 'repo_accepted_types', RepoTypeRoutePredicate)
849 config.add_route_predicate(
849 config.add_route_predicate(
850 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
850 'repo_forbid_when_archived', RepoForbidArchivedRoutePredicate)
851 config.add_route_predicate(
851 config.add_route_predicate(
852 'repo_group_route', RepoGroupRoutePredicate)
852 'repo_group_route', RepoGroupRoutePredicate)
853 config.add_route_predicate(
853 config.add_route_predicate(
854 'user_group_route', UserGroupRoutePredicate)
854 'user_group_route', UserGroupRoutePredicate)
855 config.add_route_predicate(
855 config.add_route_predicate(
856 'user_route_with_default', UserRouteWithDefaultPredicate)
856 'user_route_with_default', UserRouteWithDefaultPredicate)
857 config.add_route_predicate(
857 config.add_route_predicate(
858 'user_route', UserRoutePredicate)
858 'user_route', UserRoutePredicate)
@@ -1,29 +1,29 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 from zope.interface import Interface
21 from zope.interface import Interface
22
22
23
23
24 class IAdminNavigationRegistry(Interface):
24 class IAdminNavigationRegistry(Interface):
25 """
25 """
26 Interface for the admin navigation registry. Currently this is only
26 Interface for the admin navigation registry. Currently this is only
27 used to register and retrieve it via pyramids registry.
27 used to register and retrieve it via pyramids registry.
28 """
28 """
29 pass
29 pass
@@ -1,148 +1,147 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2016-2020 RhodeCode GmbH
2 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21
20
22 import logging
21 import logging
23 import collections
22 import collections
24
23
25 from zope.interface import implementer
24 from zope.interface import implementer
26
25
27 from rhodecode.apps._base.interfaces import IAdminNavigationRegistry
26 from rhodecode.apps._base.interfaces import IAdminNavigationRegistry
28 from rhodecode.lib.utils2 import str2bool
27 from rhodecode.lib.utils2 import str2bool
29 from rhodecode.translation import _
28 from rhodecode.translation import _
30
29
31
30
32 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
33
32
34 NavListEntry = collections.namedtuple(
33 NavListEntry = collections.namedtuple(
35 'NavListEntry', ['key', 'name', 'url', 'active_list'])
34 'NavListEntry', ['key', 'name', 'url', 'active_list'])
36
35
37
36
38 class NavEntry(object):
37 class NavEntry(object):
39 """
38 """
40 Represents an entry in the admin navigation.
39 Represents an entry in the admin navigation.
41
40
42 :param key: Unique identifier used to store reference in an OrderedDict.
41 :param key: Unique identifier used to store reference in an OrderedDict.
43 :param name: Display name, usually a translation string.
42 :param name: Display name, usually a translation string.
44 :param view_name: Name of the view, used generate the URL.
43 :param view_name: Name of the view, used generate the URL.
45 :param active_list: list of urls that we select active for this element
44 :param active_list: list of urls that we select active for this element
46 """
45 """
47
46
48 def __init__(self, key, name, view_name, active_list=None):
47 def __init__(self, key, name, view_name, active_list=None):
49 self.key = key
48 self.key = key
50 self.name = name
49 self.name = name
51 self.view_name = view_name
50 self.view_name = view_name
52 self._active_list = active_list or []
51 self._active_list = active_list or []
53
52
54 def generate_url(self, request):
53 def generate_url(self, request):
55 return request.route_path(self.view_name)
54 return request.route_path(self.view_name)
56
55
57 def get_localized_name(self, request):
56 def get_localized_name(self, request):
58 return request.translate(self.name)
57 return request.translate(self.name)
59
58
60 @property
59 @property
61 def active_list(self):
60 def active_list(self):
62 active_list = [self.key]
61 active_list = [self.key]
63 if self._active_list:
62 if self._active_list:
64 active_list = self._active_list
63 active_list = self._active_list
65 return active_list
64 return active_list
66
65
67
66
68 @implementer(IAdminNavigationRegistry)
67 @implementer(IAdminNavigationRegistry)
69 class NavigationRegistry(object):
68 class NavigationRegistry(object):
70
69
71 _base_entries = [
70 _base_entries = [
72 NavEntry('global', _('Global'),
71 NavEntry('global', _('Global'),
73 'admin_settings_global'),
72 'admin_settings_global'),
74 NavEntry('vcs', _('VCS'),
73 NavEntry('vcs', _('VCS'),
75 'admin_settings_vcs'),
74 'admin_settings_vcs'),
76 NavEntry('visual', _('Visual'),
75 NavEntry('visual', _('Visual'),
77 'admin_settings_visual'),
76 'admin_settings_visual'),
78 NavEntry('mapping', _('Remap and Rescan'),
77 NavEntry('mapping', _('Remap and Rescan'),
79 'admin_settings_mapping'),
78 'admin_settings_mapping'),
80 NavEntry('issuetracker', _('Issue Tracker'),
79 NavEntry('issuetracker', _('Issue Tracker'),
81 'admin_settings_issuetracker'),
80 'admin_settings_issuetracker'),
82 NavEntry('email', _('Email'),
81 NavEntry('email', _('Email'),
83 'admin_settings_email'),
82 'admin_settings_email'),
84 NavEntry('hooks', _('Hooks'),
83 NavEntry('hooks', _('Hooks'),
85 'admin_settings_hooks'),
84 'admin_settings_hooks'),
86 NavEntry('search', _('Full Text Search'),
85 NavEntry('search', _('Full Text Search'),
87 'admin_settings_search'),
86 'admin_settings_search'),
88 NavEntry('system', _('System Info'),
87 NavEntry('system', _('System Info'),
89 'admin_settings_system'),
88 'admin_settings_system'),
90 NavEntry('exceptions', _('Exceptions Tracker'),
89 NavEntry('exceptions', _('Exceptions Tracker'),
91 'admin_settings_exception_tracker',
90 'admin_settings_exception_tracker',
92 active_list=['exceptions', 'exceptions_browse']),
91 active_list=['exceptions', 'exceptions_browse']),
93 NavEntry('process_management', _('Processes'),
92 NavEntry('process_management', _('Processes'),
94 'admin_settings_process_management'),
93 'admin_settings_process_management'),
95 NavEntry('sessions', _('User Sessions'),
94 NavEntry('sessions', _('User Sessions'),
96 'admin_settings_sessions'),
95 'admin_settings_sessions'),
97 NavEntry('open_source', _('Open Source Licenses'),
96 NavEntry('open_source', _('Open Source Licenses'),
98 'admin_settings_open_source'),
97 'admin_settings_open_source'),
99 NavEntry('automation', _('Automation'),
98 NavEntry('automation', _('Automation'),
100 'admin_settings_automation')
99 'admin_settings_automation')
101 ]
100 ]
102
101
103 _labs_entry = NavEntry('labs', _('Labs'),
102 _labs_entry = NavEntry('labs', _('Labs'),
104 'admin_settings_labs')
103 'admin_settings_labs')
105
104
106 def __init__(self, labs_active=False):
105 def __init__(self, labs_active=False):
107 self._registered_entries = collections.OrderedDict()
106 self._registered_entries = collections.OrderedDict()
108 for item in self.__class__._base_entries:
107 for item in self.__class__._base_entries:
109 self._registered_entries[item.key] = item
108 self._registered_entries[item.key] = item
110
109
111 if labs_active:
110 if labs_active:
112 self.add_entry(self._labs_entry)
111 self.add_entry(self._labs_entry)
113
112
114 def add_entry(self, entry):
113 def add_entry(self, entry):
115 self._registered_entries[entry.key] = entry
114 self._registered_entries[entry.key] = entry
116
115
117 def get_navlist(self, request):
116 def get_navlist(self, request):
118 nav_list = [
117 nav_list = [
119 NavListEntry(i.key, i.get_localized_name(request),
118 NavListEntry(i.key, i.get_localized_name(request),
120 i.generate_url(request), i.active_list)
119 i.generate_url(request), i.active_list)
121 for i in self._registered_entries.values()]
120 for i in self._registered_entries.values()]
122 return nav_list
121 return nav_list
123
122
124
123
125 def navigation_registry(request, registry=None):
124 def navigation_registry(request, registry=None):
126 """
125 """
127 Helper that returns the admin navigation registry.
126 Helper that returns the admin navigation registry.
128 """
127 """
129 pyramid_registry = registry or request.registry
128 pyramid_registry = registry or request.registry
130 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
129 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
131 return nav_registry
130 return nav_registry
132
131
133
132
134 def navigation_list(request):
133 def navigation_list(request):
135 """
134 """
136 Helper that returns the admin navigation as list of NavListEntry objects.
135 Helper that returns the admin navigation as list of NavListEntry objects.
137 """
136 """
138 return navigation_registry(request).get_navlist(request)
137 return navigation_registry(request).get_navlist(request)
139
138
140
139
141 def includeme(config):
140 def includeme(config):
142 # Create admin navigation registry and add it to the pyramid registry.
141 # Create admin navigation registry and add it to the pyramid registry.
143 settings = config.get_settings()
142 settings = config.get_settings()
144 labs_active = str2bool(settings.get('labs_settings_active', False))
143 labs_active = str2bool(settings.get('labs_settings_active', False))
145 navigation_registry_instance = NavigationRegistry(labs_active=labs_active)
144 navigation_registry_instance = NavigationRegistry(labs_active=labs_active)
146 config.registry.registerUtility(navigation_registry_instance)
145 config.registry.registerUtility(navigation_registry_instance)
147 log.debug('Created new navigation instance, %s', navigation_registry_instance)
146 log.debug('Created new navigation instance, %s', navigation_registry_instance)
148
147
@@ -1,56 +1,56 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from rhodecode import events
23 from rhodecode import events
24 from rhodecode.lib import rc_cache
24 from rhodecode.lib import rc_cache
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28 # names of namespaces used for different permission related cached
28 # names of namespaces used for different permission related cached
29 # during flush operation we need to take care of all those
29 # during flush operation we need to take care of all those
30 cache_namespaces = [
30 cache_namespaces = [
31 'cache_user_auth.{}',
31 'cache_user_auth.{}',
32 'cache_user_repo_acl_ids.{}',
32 'cache_user_repo_acl_ids.{}',
33 'cache_user_user_group_acl_ids.{}',
33 'cache_user_user_group_acl_ids.{}',
34 'cache_user_repo_group_acl_ids.{}'
34 'cache_user_repo_group_acl_ids.{}'
35 ]
35 ]
36
36
37
37
38 def trigger_user_permission_flush(event):
38 def trigger_user_permission_flush(event):
39 """
39 """
40 Subscriber to the `UserPermissionsChange`. This triggers the
40 Subscriber to the `UserPermissionsChange`. This triggers the
41 automatic flush of permission caches, so the users affected receive new permissions
41 automatic flush of permission caches, so the users affected receive new permissions
42 Right Away
42 Right Away
43 """
43 """
44 invalidate = True
44 invalidate = True
45 affected_user_ids = set(event.user_ids)
45 affected_user_ids = set(event.user_ids)
46 for user_id in affected_user_ids:
46 for user_id in affected_user_ids:
47 for cache_namespace_uid_tmpl in cache_namespaces:
47 for cache_namespace_uid_tmpl in cache_namespaces:
48 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
48 cache_namespace_uid = cache_namespace_uid_tmpl.format(user_id)
49 del_keys = rc_cache.clear_cache_namespace(
49 del_keys = rc_cache.clear_cache_namespace(
50 'cache_perms', cache_namespace_uid, invalidate=invalidate)
50 'cache_perms', cache_namespace_uid, invalidate=invalidate)
51 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
51 log.debug('Invalidated %s cache keys for user_id: %s and namespace %s',
52 del_keys, user_id, cache_namespace_uid)
52 del_keys, user_id, cache_namespace_uid)
53
53
54
54
55 def includeme(config):
55 def includeme(config):
56 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
56 config.add_subscriber(trigger_user_permission_flush, events.UserPermissionsChange)
@@ -1,1084 +1,1084 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 from rhodecode.apps._base import ADMIN_PREFIX
22 from rhodecode.apps._base import ADMIN_PREFIX
23
23
24
24
25 def admin_routes(config):
25 def admin_routes(config):
26 """
26 """
27 Admin prefixed routes
27 Admin prefixed routes
28 """
28 """
29 from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView
29 from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView
30 from rhodecode.apps.admin.views.artifacts import AdminArtifactsView
30 from rhodecode.apps.admin.views.artifacts import AdminArtifactsView
31 from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView
31 from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView
32 from rhodecode.apps.admin.views.exception_tracker import ExceptionsTrackerView
32 from rhodecode.apps.admin.views.exception_tracker import ExceptionsTrackerView
33 from rhodecode.apps.admin.views.main_views import AdminMainView
33 from rhodecode.apps.admin.views.main_views import AdminMainView
34 from rhodecode.apps.admin.views.open_source_licenses import OpenSourceLicensesAdminSettingsView
34 from rhodecode.apps.admin.views.open_source_licenses import OpenSourceLicensesAdminSettingsView
35 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
35 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
36 from rhodecode.apps.admin.views.process_management import AdminProcessManagementView
36 from rhodecode.apps.admin.views.process_management import AdminProcessManagementView
37 from rhodecode.apps.admin.views.repo_groups import AdminRepoGroupsView
37 from rhodecode.apps.admin.views.repo_groups import AdminRepoGroupsView
38 from rhodecode.apps.admin.views.repositories import AdminReposView
38 from rhodecode.apps.admin.views.repositories import AdminReposView
39 from rhodecode.apps.admin.views.sessions import AdminSessionSettingsView
39 from rhodecode.apps.admin.views.sessions import AdminSessionSettingsView
40 from rhodecode.apps.admin.views.settings import AdminSettingsView
40 from rhodecode.apps.admin.views.settings import AdminSettingsView
41 from rhodecode.apps.admin.views.svn_config import AdminSvnConfigView
41 from rhodecode.apps.admin.views.svn_config import AdminSvnConfigView
42 from rhodecode.apps.admin.views.system_info import AdminSystemInfoSettingsView
42 from rhodecode.apps.admin.views.system_info import AdminSystemInfoSettingsView
43 from rhodecode.apps.admin.views.user_groups import AdminUserGroupsView
43 from rhodecode.apps.admin.views.user_groups import AdminUserGroupsView
44 from rhodecode.apps.admin.views.users import AdminUsersView, UsersView
44 from rhodecode.apps.admin.views.users import AdminUsersView, UsersView
45
45
46 config.add_route(
46 config.add_route(
47 name='admin_audit_logs',
47 name='admin_audit_logs',
48 pattern='/audit_logs')
48 pattern='/audit_logs')
49 config.add_view(
49 config.add_view(
50 AdminAuditLogsView,
50 AdminAuditLogsView,
51 attr='admin_audit_logs',
51 attr='admin_audit_logs',
52 route_name='admin_audit_logs', request_method='GET',
52 route_name='admin_audit_logs', request_method='GET',
53 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
53 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
54
54
55 config.add_route(
55 config.add_route(
56 name='admin_audit_log_entry',
56 name='admin_audit_log_entry',
57 pattern='/audit_logs/{audit_log_id}')
57 pattern='/audit_logs/{audit_log_id}')
58 config.add_view(
58 config.add_view(
59 AdminAuditLogsView,
59 AdminAuditLogsView,
60 attr='admin_audit_log_entry',
60 attr='admin_audit_log_entry',
61 route_name='admin_audit_log_entry', request_method='GET',
61 route_name='admin_audit_log_entry', request_method='GET',
62 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
62 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
63
63
64 # Artifacts EE feature
64 # Artifacts EE feature
65 config.add_route(
65 config.add_route(
66 'admin_artifacts',
66 'admin_artifacts',
67 pattern=ADMIN_PREFIX + '/artifacts')
67 pattern=ADMIN_PREFIX + '/artifacts')
68 config.add_route(
68 config.add_route(
69 'admin_artifacts_show_all',
69 'admin_artifacts_show_all',
70 pattern=ADMIN_PREFIX + '/artifacts')
70 pattern=ADMIN_PREFIX + '/artifacts')
71 config.add_view(
71 config.add_view(
72 AdminArtifactsView,
72 AdminArtifactsView,
73 attr='artifacts',
73 attr='artifacts',
74 route_name='admin_artifacts', request_method='GET',
74 route_name='admin_artifacts', request_method='GET',
75 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
75 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
76 config.add_view(
76 config.add_view(
77 AdminArtifactsView,
77 AdminArtifactsView,
78 attr='artifacts',
78 attr='artifacts',
79 route_name='admin_artifacts_show_all', request_method='GET',
79 route_name='admin_artifacts_show_all', request_method='GET',
80 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
80 renderer='rhodecode:templates/admin/artifacts/artifacts.mako')
81 # EE views
81 # EE views
82 config.add_route(
82 config.add_route(
83 name='admin_artifacts_show_info',
83 name='admin_artifacts_show_info',
84 pattern=ADMIN_PREFIX + '/artifacts/{uid}')
84 pattern=ADMIN_PREFIX + '/artifacts/{uid}')
85 config.add_route(
85 config.add_route(
86 name='admin_artifacts_delete',
86 name='admin_artifacts_delete',
87 pattern=ADMIN_PREFIX + '/artifacts/{uid}/delete')
87 pattern=ADMIN_PREFIX + '/artifacts/{uid}/delete')
88 config.add_route(
88 config.add_route(
89 name='admin_artifacts_update',
89 name='admin_artifacts_update',
90 pattern=ADMIN_PREFIX + '/artifacts/{uid}/update')
90 pattern=ADMIN_PREFIX + '/artifacts/{uid}/update')
91
91
92 config.add_route(
92 config.add_route(
93 name='admin_settings_open_source',
93 name='admin_settings_open_source',
94 pattern='/settings/open_source')
94 pattern='/settings/open_source')
95 config.add_view(
95 config.add_view(
96 OpenSourceLicensesAdminSettingsView,
96 OpenSourceLicensesAdminSettingsView,
97 attr='open_source_licenses',
97 attr='open_source_licenses',
98 route_name='admin_settings_open_source', request_method='GET',
98 route_name='admin_settings_open_source', request_method='GET',
99 renderer='rhodecode:templates/admin/settings/settings.mako')
99 renderer='rhodecode:templates/admin/settings/settings.mako')
100
100
101 config.add_route(
101 config.add_route(
102 name='admin_settings_vcs_svn_generate_cfg',
102 name='admin_settings_vcs_svn_generate_cfg',
103 pattern='/settings/vcs/svn_generate_cfg')
103 pattern='/settings/vcs/svn_generate_cfg')
104 config.add_view(
104 config.add_view(
105 AdminSvnConfigView,
105 AdminSvnConfigView,
106 attr='vcs_svn_generate_config',
106 attr='vcs_svn_generate_config',
107 route_name='admin_settings_vcs_svn_generate_cfg',
107 route_name='admin_settings_vcs_svn_generate_cfg',
108 request_method='POST', renderer='json')
108 request_method='POST', renderer='json')
109
109
110 config.add_route(
110 config.add_route(
111 name='admin_settings_system',
111 name='admin_settings_system',
112 pattern='/settings/system')
112 pattern='/settings/system')
113 config.add_view(
113 config.add_view(
114 AdminSystemInfoSettingsView,
114 AdminSystemInfoSettingsView,
115 attr='settings_system_info',
115 attr='settings_system_info',
116 route_name='admin_settings_system', request_method='GET',
116 route_name='admin_settings_system', request_method='GET',
117 renderer='rhodecode:templates/admin/settings/settings.mako')
117 renderer='rhodecode:templates/admin/settings/settings.mako')
118
118
119 config.add_route(
119 config.add_route(
120 name='admin_settings_system_update',
120 name='admin_settings_system_update',
121 pattern='/settings/system/updates')
121 pattern='/settings/system/updates')
122 config.add_view(
122 config.add_view(
123 AdminSystemInfoSettingsView,
123 AdminSystemInfoSettingsView,
124 attr='settings_system_info_check_update',
124 attr='settings_system_info_check_update',
125 route_name='admin_settings_system_update', request_method='GET',
125 route_name='admin_settings_system_update', request_method='GET',
126 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
126 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
127
127
128 config.add_route(
128 config.add_route(
129 name='admin_settings_exception_tracker',
129 name='admin_settings_exception_tracker',
130 pattern='/settings/exceptions')
130 pattern='/settings/exceptions')
131 config.add_view(
131 config.add_view(
132 ExceptionsTrackerView,
132 ExceptionsTrackerView,
133 attr='browse_exceptions',
133 attr='browse_exceptions',
134 route_name='admin_settings_exception_tracker', request_method='GET',
134 route_name='admin_settings_exception_tracker', request_method='GET',
135 renderer='rhodecode:templates/admin/settings/settings.mako')
135 renderer='rhodecode:templates/admin/settings/settings.mako')
136
136
137 config.add_route(
137 config.add_route(
138 name='admin_settings_exception_tracker_delete_all',
138 name='admin_settings_exception_tracker_delete_all',
139 pattern='/settings/exceptions_delete_all')
139 pattern='/settings/exceptions_delete_all')
140 config.add_view(
140 config.add_view(
141 ExceptionsTrackerView,
141 ExceptionsTrackerView,
142 attr='exception_delete_all',
142 attr='exception_delete_all',
143 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
143 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
144 renderer='rhodecode:templates/admin/settings/settings.mako')
144 renderer='rhodecode:templates/admin/settings/settings.mako')
145
145
146 config.add_route(
146 config.add_route(
147 name='admin_settings_exception_tracker_show',
147 name='admin_settings_exception_tracker_show',
148 pattern='/settings/exceptions/{exception_id}')
148 pattern='/settings/exceptions/{exception_id}')
149 config.add_view(
149 config.add_view(
150 ExceptionsTrackerView,
150 ExceptionsTrackerView,
151 attr='exception_show',
151 attr='exception_show',
152 route_name='admin_settings_exception_tracker_show', request_method='GET',
152 route_name='admin_settings_exception_tracker_show', request_method='GET',
153 renderer='rhodecode:templates/admin/settings/settings.mako')
153 renderer='rhodecode:templates/admin/settings/settings.mako')
154
154
155 config.add_route(
155 config.add_route(
156 name='admin_settings_exception_tracker_delete',
156 name='admin_settings_exception_tracker_delete',
157 pattern='/settings/exceptions/{exception_id}/delete')
157 pattern='/settings/exceptions/{exception_id}/delete')
158 config.add_view(
158 config.add_view(
159 ExceptionsTrackerView,
159 ExceptionsTrackerView,
160 attr='exception_delete',
160 attr='exception_delete',
161 route_name='admin_settings_exception_tracker_delete', request_method='POST',
161 route_name='admin_settings_exception_tracker_delete', request_method='POST',
162 renderer='rhodecode:templates/admin/settings/settings.mako')
162 renderer='rhodecode:templates/admin/settings/settings.mako')
163
163
164 config.add_route(
164 config.add_route(
165 name='admin_settings_sessions',
165 name='admin_settings_sessions',
166 pattern='/settings/sessions')
166 pattern='/settings/sessions')
167 config.add_view(
167 config.add_view(
168 AdminSessionSettingsView,
168 AdminSessionSettingsView,
169 attr='settings_sessions',
169 attr='settings_sessions',
170 route_name='admin_settings_sessions', request_method='GET',
170 route_name='admin_settings_sessions', request_method='GET',
171 renderer='rhodecode:templates/admin/settings/settings.mako')
171 renderer='rhodecode:templates/admin/settings/settings.mako')
172
172
173 config.add_route(
173 config.add_route(
174 name='admin_settings_sessions_cleanup',
174 name='admin_settings_sessions_cleanup',
175 pattern='/settings/sessions/cleanup')
175 pattern='/settings/sessions/cleanup')
176 config.add_view(
176 config.add_view(
177 AdminSessionSettingsView,
177 AdminSessionSettingsView,
178 attr='settings_sessions_cleanup',
178 attr='settings_sessions_cleanup',
179 route_name='admin_settings_sessions_cleanup', request_method='POST')
179 route_name='admin_settings_sessions_cleanup', request_method='POST')
180
180
181 config.add_route(
181 config.add_route(
182 name='admin_settings_process_management',
182 name='admin_settings_process_management',
183 pattern='/settings/process_management')
183 pattern='/settings/process_management')
184 config.add_view(
184 config.add_view(
185 AdminProcessManagementView,
185 AdminProcessManagementView,
186 attr='process_management',
186 attr='process_management',
187 route_name='admin_settings_process_management', request_method='GET',
187 route_name='admin_settings_process_management', request_method='GET',
188 renderer='rhodecode:templates/admin/settings/settings.mako')
188 renderer='rhodecode:templates/admin/settings/settings.mako')
189
189
190 config.add_route(
190 config.add_route(
191 name='admin_settings_process_management_data',
191 name='admin_settings_process_management_data',
192 pattern='/settings/process_management/data')
192 pattern='/settings/process_management/data')
193 config.add_view(
193 config.add_view(
194 AdminProcessManagementView,
194 AdminProcessManagementView,
195 attr='process_management_data',
195 attr='process_management_data',
196 route_name='admin_settings_process_management_data', request_method='GET',
196 route_name='admin_settings_process_management_data', request_method='GET',
197 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
197 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
198
198
199 config.add_route(
199 config.add_route(
200 name='admin_settings_process_management_signal',
200 name='admin_settings_process_management_signal',
201 pattern='/settings/process_management/signal')
201 pattern='/settings/process_management/signal')
202 config.add_view(
202 config.add_view(
203 AdminProcessManagementView,
203 AdminProcessManagementView,
204 attr='process_management_signal',
204 attr='process_management_signal',
205 route_name='admin_settings_process_management_signal',
205 route_name='admin_settings_process_management_signal',
206 request_method='POST', renderer='json_ext')
206 request_method='POST', renderer='json_ext')
207
207
208 config.add_route(
208 config.add_route(
209 name='admin_settings_process_management_master_signal',
209 name='admin_settings_process_management_master_signal',
210 pattern='/settings/process_management/master_signal')
210 pattern='/settings/process_management/master_signal')
211 config.add_view(
211 config.add_view(
212 AdminProcessManagementView,
212 AdminProcessManagementView,
213 attr='process_management_master_signal',
213 attr='process_management_master_signal',
214 route_name='admin_settings_process_management_master_signal',
214 route_name='admin_settings_process_management_master_signal',
215 request_method='POST', renderer='json_ext')
215 request_method='POST', renderer='json_ext')
216
216
217 # default settings
217 # default settings
218 config.add_route(
218 config.add_route(
219 name='admin_defaults_repositories',
219 name='admin_defaults_repositories',
220 pattern='/defaults/repositories')
220 pattern='/defaults/repositories')
221 config.add_view(
221 config.add_view(
222 AdminDefaultSettingsView,
222 AdminDefaultSettingsView,
223 attr='defaults_repository_show',
223 attr='defaults_repository_show',
224 route_name='admin_defaults_repositories', request_method='GET',
224 route_name='admin_defaults_repositories', request_method='GET',
225 renderer='rhodecode:templates/admin/defaults/defaults.mako')
225 renderer='rhodecode:templates/admin/defaults/defaults.mako')
226
226
227 config.add_route(
227 config.add_route(
228 name='admin_defaults_repositories_update',
228 name='admin_defaults_repositories_update',
229 pattern='/defaults/repositories/update')
229 pattern='/defaults/repositories/update')
230 config.add_view(
230 config.add_view(
231 AdminDefaultSettingsView,
231 AdminDefaultSettingsView,
232 attr='defaults_repository_update',
232 attr='defaults_repository_update',
233 route_name='admin_defaults_repositories_update', request_method='POST',
233 route_name='admin_defaults_repositories_update', request_method='POST',
234 renderer='rhodecode:templates/admin/defaults/defaults.mako')
234 renderer='rhodecode:templates/admin/defaults/defaults.mako')
235
235
236 # admin settings
236 # admin settings
237
237
238 config.add_route(
238 config.add_route(
239 name='admin_settings',
239 name='admin_settings',
240 pattern='/settings')
240 pattern='/settings')
241 config.add_view(
241 config.add_view(
242 AdminSettingsView,
242 AdminSettingsView,
243 attr='settings_global',
243 attr='settings_global',
244 route_name='admin_settings', request_method='GET',
244 route_name='admin_settings', request_method='GET',
245 renderer='rhodecode:templates/admin/settings/settings.mako')
245 renderer='rhodecode:templates/admin/settings/settings.mako')
246
246
247 config.add_route(
247 config.add_route(
248 name='admin_settings_update',
248 name='admin_settings_update',
249 pattern='/settings/update')
249 pattern='/settings/update')
250 config.add_view(
250 config.add_view(
251 AdminSettingsView,
251 AdminSettingsView,
252 attr='settings_global_update',
252 attr='settings_global_update',
253 route_name='admin_settings_update', request_method='POST',
253 route_name='admin_settings_update', request_method='POST',
254 renderer='rhodecode:templates/admin/settings/settings.mako')
254 renderer='rhodecode:templates/admin/settings/settings.mako')
255
255
256 config.add_route(
256 config.add_route(
257 name='admin_settings_global',
257 name='admin_settings_global',
258 pattern='/settings/global')
258 pattern='/settings/global')
259 config.add_view(
259 config.add_view(
260 AdminSettingsView,
260 AdminSettingsView,
261 attr='settings_global',
261 attr='settings_global',
262 route_name='admin_settings_global', request_method='GET',
262 route_name='admin_settings_global', request_method='GET',
263 renderer='rhodecode:templates/admin/settings/settings.mako')
263 renderer='rhodecode:templates/admin/settings/settings.mako')
264
264
265 config.add_route(
265 config.add_route(
266 name='admin_settings_global_update',
266 name='admin_settings_global_update',
267 pattern='/settings/global/update')
267 pattern='/settings/global/update')
268 config.add_view(
268 config.add_view(
269 AdminSettingsView,
269 AdminSettingsView,
270 attr='settings_global_update',
270 attr='settings_global_update',
271 route_name='admin_settings_global_update', request_method='POST',
271 route_name='admin_settings_global_update', request_method='POST',
272 renderer='rhodecode:templates/admin/settings/settings.mako')
272 renderer='rhodecode:templates/admin/settings/settings.mako')
273
273
274 config.add_route(
274 config.add_route(
275 name='admin_settings_vcs',
275 name='admin_settings_vcs',
276 pattern='/settings/vcs')
276 pattern='/settings/vcs')
277 config.add_view(
277 config.add_view(
278 AdminSettingsView,
278 AdminSettingsView,
279 attr='settings_vcs',
279 attr='settings_vcs',
280 route_name='admin_settings_vcs', request_method='GET',
280 route_name='admin_settings_vcs', request_method='GET',
281 renderer='rhodecode:templates/admin/settings/settings.mako')
281 renderer='rhodecode:templates/admin/settings/settings.mako')
282
282
283 config.add_route(
283 config.add_route(
284 name='admin_settings_vcs_update',
284 name='admin_settings_vcs_update',
285 pattern='/settings/vcs/update')
285 pattern='/settings/vcs/update')
286 config.add_view(
286 config.add_view(
287 AdminSettingsView,
287 AdminSettingsView,
288 attr='settings_vcs_update',
288 attr='settings_vcs_update',
289 route_name='admin_settings_vcs_update', request_method='POST',
289 route_name='admin_settings_vcs_update', request_method='POST',
290 renderer='rhodecode:templates/admin/settings/settings.mako')
290 renderer='rhodecode:templates/admin/settings/settings.mako')
291
291
292 config.add_route(
292 config.add_route(
293 name='admin_settings_vcs_svn_pattern_delete',
293 name='admin_settings_vcs_svn_pattern_delete',
294 pattern='/settings/vcs/svn_pattern_delete')
294 pattern='/settings/vcs/svn_pattern_delete')
295 config.add_view(
295 config.add_view(
296 AdminSettingsView,
296 AdminSettingsView,
297 attr='settings_vcs_delete_svn_pattern',
297 attr='settings_vcs_delete_svn_pattern',
298 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
298 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
299 renderer='json_ext', xhr=True)
299 renderer='json_ext', xhr=True)
300
300
301 config.add_route(
301 config.add_route(
302 name='admin_settings_mapping',
302 name='admin_settings_mapping',
303 pattern='/settings/mapping')
303 pattern='/settings/mapping')
304 config.add_view(
304 config.add_view(
305 AdminSettingsView,
305 AdminSettingsView,
306 attr='settings_mapping',
306 attr='settings_mapping',
307 route_name='admin_settings_mapping', request_method='GET',
307 route_name='admin_settings_mapping', request_method='GET',
308 renderer='rhodecode:templates/admin/settings/settings.mako')
308 renderer='rhodecode:templates/admin/settings/settings.mako')
309
309
310 config.add_route(
310 config.add_route(
311 name='admin_settings_mapping_update',
311 name='admin_settings_mapping_update',
312 pattern='/settings/mapping/update')
312 pattern='/settings/mapping/update')
313 config.add_view(
313 config.add_view(
314 AdminSettingsView,
314 AdminSettingsView,
315 attr='settings_mapping_update',
315 attr='settings_mapping_update',
316 route_name='admin_settings_mapping_update', request_method='POST',
316 route_name='admin_settings_mapping_update', request_method='POST',
317 renderer='rhodecode:templates/admin/settings/settings.mako')
317 renderer='rhodecode:templates/admin/settings/settings.mako')
318
318
319 config.add_route(
319 config.add_route(
320 name='admin_settings_visual',
320 name='admin_settings_visual',
321 pattern='/settings/visual')
321 pattern='/settings/visual')
322 config.add_view(
322 config.add_view(
323 AdminSettingsView,
323 AdminSettingsView,
324 attr='settings_visual',
324 attr='settings_visual',
325 route_name='admin_settings_visual', request_method='GET',
325 route_name='admin_settings_visual', request_method='GET',
326 renderer='rhodecode:templates/admin/settings/settings.mako')
326 renderer='rhodecode:templates/admin/settings/settings.mako')
327
327
328 config.add_route(
328 config.add_route(
329 name='admin_settings_visual_update',
329 name='admin_settings_visual_update',
330 pattern='/settings/visual/update')
330 pattern='/settings/visual/update')
331 config.add_view(
331 config.add_view(
332 AdminSettingsView,
332 AdminSettingsView,
333 attr='settings_visual_update',
333 attr='settings_visual_update',
334 route_name='admin_settings_visual_update', request_method='POST',
334 route_name='admin_settings_visual_update', request_method='POST',
335 renderer='rhodecode:templates/admin/settings/settings.mako')
335 renderer='rhodecode:templates/admin/settings/settings.mako')
336
336
337 config.add_route(
337 config.add_route(
338 name='admin_settings_issuetracker',
338 name='admin_settings_issuetracker',
339 pattern='/settings/issue-tracker')
339 pattern='/settings/issue-tracker')
340 config.add_view(
340 config.add_view(
341 AdminSettingsView,
341 AdminSettingsView,
342 attr='settings_issuetracker',
342 attr='settings_issuetracker',
343 route_name='admin_settings_issuetracker', request_method='GET',
343 route_name='admin_settings_issuetracker', request_method='GET',
344 renderer='rhodecode:templates/admin/settings/settings.mako')
344 renderer='rhodecode:templates/admin/settings/settings.mako')
345
345
346 config.add_route(
346 config.add_route(
347 name='admin_settings_issuetracker_update',
347 name='admin_settings_issuetracker_update',
348 pattern='/settings/issue-tracker/update')
348 pattern='/settings/issue-tracker/update')
349 config.add_view(
349 config.add_view(
350 AdminSettingsView,
350 AdminSettingsView,
351 attr='settings_issuetracker_update',
351 attr='settings_issuetracker_update',
352 route_name='admin_settings_issuetracker_update', request_method='POST',
352 route_name='admin_settings_issuetracker_update', request_method='POST',
353 renderer='rhodecode:templates/admin/settings/settings.mako')
353 renderer='rhodecode:templates/admin/settings/settings.mako')
354
354
355 config.add_route(
355 config.add_route(
356 name='admin_settings_issuetracker_test',
356 name='admin_settings_issuetracker_test',
357 pattern='/settings/issue-tracker/test')
357 pattern='/settings/issue-tracker/test')
358 config.add_view(
358 config.add_view(
359 AdminSettingsView,
359 AdminSettingsView,
360 attr='settings_issuetracker_test',
360 attr='settings_issuetracker_test',
361 route_name='admin_settings_issuetracker_test', request_method='POST',
361 route_name='admin_settings_issuetracker_test', request_method='POST',
362 renderer='string', xhr=True)
362 renderer='string', xhr=True)
363
363
364 config.add_route(
364 config.add_route(
365 name='admin_settings_issuetracker_delete',
365 name='admin_settings_issuetracker_delete',
366 pattern='/settings/issue-tracker/delete')
366 pattern='/settings/issue-tracker/delete')
367 config.add_view(
367 config.add_view(
368 AdminSettingsView,
368 AdminSettingsView,
369 attr='settings_issuetracker_delete',
369 attr='settings_issuetracker_delete',
370 route_name='admin_settings_issuetracker_delete', request_method='POST',
370 route_name='admin_settings_issuetracker_delete', request_method='POST',
371 renderer='json_ext', xhr=True)
371 renderer='json_ext', xhr=True)
372
372
373 config.add_route(
373 config.add_route(
374 name='admin_settings_email',
374 name='admin_settings_email',
375 pattern='/settings/email')
375 pattern='/settings/email')
376 config.add_view(
376 config.add_view(
377 AdminSettingsView,
377 AdminSettingsView,
378 attr='settings_email',
378 attr='settings_email',
379 route_name='admin_settings_email', request_method='GET',
379 route_name='admin_settings_email', request_method='GET',
380 renderer='rhodecode:templates/admin/settings/settings.mako')
380 renderer='rhodecode:templates/admin/settings/settings.mako')
381
381
382 config.add_route(
382 config.add_route(
383 name='admin_settings_email_update',
383 name='admin_settings_email_update',
384 pattern='/settings/email/update')
384 pattern='/settings/email/update')
385 config.add_view(
385 config.add_view(
386 AdminSettingsView,
386 AdminSettingsView,
387 attr='settings_email_update',
387 attr='settings_email_update',
388 route_name='admin_settings_email_update', request_method='POST',
388 route_name='admin_settings_email_update', request_method='POST',
389 renderer='rhodecode:templates/admin/settings/settings.mako')
389 renderer='rhodecode:templates/admin/settings/settings.mako')
390
390
391 config.add_route(
391 config.add_route(
392 name='admin_settings_hooks',
392 name='admin_settings_hooks',
393 pattern='/settings/hooks')
393 pattern='/settings/hooks')
394 config.add_view(
394 config.add_view(
395 AdminSettingsView,
395 AdminSettingsView,
396 attr='settings_hooks',
396 attr='settings_hooks',
397 route_name='admin_settings_hooks', request_method='GET',
397 route_name='admin_settings_hooks', request_method='GET',
398 renderer='rhodecode:templates/admin/settings/settings.mako')
398 renderer='rhodecode:templates/admin/settings/settings.mako')
399
399
400 config.add_route(
400 config.add_route(
401 name='admin_settings_hooks_update',
401 name='admin_settings_hooks_update',
402 pattern='/settings/hooks/update')
402 pattern='/settings/hooks/update')
403 config.add_view(
403 config.add_view(
404 AdminSettingsView,
404 AdminSettingsView,
405 attr='settings_hooks_update',
405 attr='settings_hooks_update',
406 route_name='admin_settings_hooks_update', request_method='POST',
406 route_name='admin_settings_hooks_update', request_method='POST',
407 renderer='rhodecode:templates/admin/settings/settings.mako')
407 renderer='rhodecode:templates/admin/settings/settings.mako')
408
408
409 config.add_route(
409 config.add_route(
410 name='admin_settings_hooks_delete',
410 name='admin_settings_hooks_delete',
411 pattern='/settings/hooks/delete')
411 pattern='/settings/hooks/delete')
412 config.add_view(
412 config.add_view(
413 AdminSettingsView,
413 AdminSettingsView,
414 attr='settings_hooks_update',
414 attr='settings_hooks_update',
415 route_name='admin_settings_hooks_delete', request_method='POST',
415 route_name='admin_settings_hooks_delete', request_method='POST',
416 renderer='rhodecode:templates/admin/settings/settings.mako')
416 renderer='rhodecode:templates/admin/settings/settings.mako')
417
417
418 config.add_route(
418 config.add_route(
419 name='admin_settings_search',
419 name='admin_settings_search',
420 pattern='/settings/search')
420 pattern='/settings/search')
421 config.add_view(
421 config.add_view(
422 AdminSettingsView,
422 AdminSettingsView,
423 attr='settings_search',
423 attr='settings_search',
424 route_name='admin_settings_search', request_method='GET',
424 route_name='admin_settings_search', request_method='GET',
425 renderer='rhodecode:templates/admin/settings/settings.mako')
425 renderer='rhodecode:templates/admin/settings/settings.mako')
426
426
427 config.add_route(
427 config.add_route(
428 name='admin_settings_labs',
428 name='admin_settings_labs',
429 pattern='/settings/labs')
429 pattern='/settings/labs')
430 config.add_view(
430 config.add_view(
431 AdminSettingsView,
431 AdminSettingsView,
432 attr='settings_labs',
432 attr='settings_labs',
433 route_name='admin_settings_labs', request_method='GET',
433 route_name='admin_settings_labs', request_method='GET',
434 renderer='rhodecode:templates/admin/settings/settings.mako')
434 renderer='rhodecode:templates/admin/settings/settings.mako')
435
435
436 config.add_route(
436 config.add_route(
437 name='admin_settings_labs_update',
437 name='admin_settings_labs_update',
438 pattern='/settings/labs/update')
438 pattern='/settings/labs/update')
439 config.add_view(
439 config.add_view(
440 AdminSettingsView,
440 AdminSettingsView,
441 attr='settings_labs_update',
441 attr='settings_labs_update',
442 route_name='admin_settings_labs_update', request_method='POST',
442 route_name='admin_settings_labs_update', request_method='POST',
443 renderer='rhodecode:templates/admin/settings/settings.mako')
443 renderer='rhodecode:templates/admin/settings/settings.mako')
444
444
445 # Automation EE feature
445 # Automation EE feature
446 config.add_route(
446 config.add_route(
447 'admin_settings_automation',
447 'admin_settings_automation',
448 pattern=ADMIN_PREFIX + '/settings/automation')
448 pattern=ADMIN_PREFIX + '/settings/automation')
449 config.add_view(
449 config.add_view(
450 AdminSettingsView,
450 AdminSettingsView,
451 attr='settings_automation',
451 attr='settings_automation',
452 route_name='admin_settings_automation', request_method='GET',
452 route_name='admin_settings_automation', request_method='GET',
453 renderer='rhodecode:templates/admin/settings/settings.mako')
453 renderer='rhodecode:templates/admin/settings/settings.mako')
454
454
455 # global permissions
455 # global permissions
456
456
457 config.add_route(
457 config.add_route(
458 name='admin_permissions_application',
458 name='admin_permissions_application',
459 pattern='/permissions/application')
459 pattern='/permissions/application')
460 config.add_view(
460 config.add_view(
461 AdminPermissionsView,
461 AdminPermissionsView,
462 attr='permissions_application',
462 attr='permissions_application',
463 route_name='admin_permissions_application', request_method='GET',
463 route_name='admin_permissions_application', request_method='GET',
464 renderer='rhodecode:templates/admin/permissions/permissions.mako')
464 renderer='rhodecode:templates/admin/permissions/permissions.mako')
465
465
466 config.add_route(
466 config.add_route(
467 name='admin_permissions_application_update',
467 name='admin_permissions_application_update',
468 pattern='/permissions/application/update')
468 pattern='/permissions/application/update')
469 config.add_view(
469 config.add_view(
470 AdminPermissionsView,
470 AdminPermissionsView,
471 attr='permissions_application_update',
471 attr='permissions_application_update',
472 route_name='admin_permissions_application_update', request_method='POST',
472 route_name='admin_permissions_application_update', request_method='POST',
473 renderer='rhodecode:templates/admin/permissions/permissions.mako')
473 renderer='rhodecode:templates/admin/permissions/permissions.mako')
474
474
475 config.add_route(
475 config.add_route(
476 name='admin_permissions_global',
476 name='admin_permissions_global',
477 pattern='/permissions/global')
477 pattern='/permissions/global')
478 config.add_view(
478 config.add_view(
479 AdminPermissionsView,
479 AdminPermissionsView,
480 attr='permissions_global',
480 attr='permissions_global',
481 route_name='admin_permissions_global', request_method='GET',
481 route_name='admin_permissions_global', request_method='GET',
482 renderer='rhodecode:templates/admin/permissions/permissions.mako')
482 renderer='rhodecode:templates/admin/permissions/permissions.mako')
483
483
484 config.add_route(
484 config.add_route(
485 name='admin_permissions_global_update',
485 name='admin_permissions_global_update',
486 pattern='/permissions/global/update')
486 pattern='/permissions/global/update')
487 config.add_view(
487 config.add_view(
488 AdminPermissionsView,
488 AdminPermissionsView,
489 attr='permissions_global_update',
489 attr='permissions_global_update',
490 route_name='admin_permissions_global_update', request_method='POST',
490 route_name='admin_permissions_global_update', request_method='POST',
491 renderer='rhodecode:templates/admin/permissions/permissions.mako')
491 renderer='rhodecode:templates/admin/permissions/permissions.mako')
492
492
493 config.add_route(
493 config.add_route(
494 name='admin_permissions_object',
494 name='admin_permissions_object',
495 pattern='/permissions/object')
495 pattern='/permissions/object')
496 config.add_view(
496 config.add_view(
497 AdminPermissionsView,
497 AdminPermissionsView,
498 attr='permissions_objects',
498 attr='permissions_objects',
499 route_name='admin_permissions_object', request_method='GET',
499 route_name='admin_permissions_object', request_method='GET',
500 renderer='rhodecode:templates/admin/permissions/permissions.mako')
500 renderer='rhodecode:templates/admin/permissions/permissions.mako')
501
501
502 config.add_route(
502 config.add_route(
503 name='admin_permissions_object_update',
503 name='admin_permissions_object_update',
504 pattern='/permissions/object/update')
504 pattern='/permissions/object/update')
505 config.add_view(
505 config.add_view(
506 AdminPermissionsView,
506 AdminPermissionsView,
507 attr='permissions_objects_update',
507 attr='permissions_objects_update',
508 route_name='admin_permissions_object_update', request_method='POST',
508 route_name='admin_permissions_object_update', request_method='POST',
509 renderer='rhodecode:templates/admin/permissions/permissions.mako')
509 renderer='rhodecode:templates/admin/permissions/permissions.mako')
510
510
511 # Branch perms EE feature
511 # Branch perms EE feature
512 config.add_route(
512 config.add_route(
513 name='admin_permissions_branch',
513 name='admin_permissions_branch',
514 pattern='/permissions/branch')
514 pattern='/permissions/branch')
515 config.add_view(
515 config.add_view(
516 AdminPermissionsView,
516 AdminPermissionsView,
517 attr='permissions_branch',
517 attr='permissions_branch',
518 route_name='admin_permissions_branch', request_method='GET',
518 route_name='admin_permissions_branch', request_method='GET',
519 renderer='rhodecode:templates/admin/permissions/permissions.mako')
519 renderer='rhodecode:templates/admin/permissions/permissions.mako')
520
520
521 config.add_route(
521 config.add_route(
522 name='admin_permissions_ips',
522 name='admin_permissions_ips',
523 pattern='/permissions/ips')
523 pattern='/permissions/ips')
524 config.add_view(
524 config.add_view(
525 AdminPermissionsView,
525 AdminPermissionsView,
526 attr='permissions_ips',
526 attr='permissions_ips',
527 route_name='admin_permissions_ips', request_method='GET',
527 route_name='admin_permissions_ips', request_method='GET',
528 renderer='rhodecode:templates/admin/permissions/permissions.mako')
528 renderer='rhodecode:templates/admin/permissions/permissions.mako')
529
529
530 config.add_route(
530 config.add_route(
531 name='admin_permissions_overview',
531 name='admin_permissions_overview',
532 pattern='/permissions/overview')
532 pattern='/permissions/overview')
533 config.add_view(
533 config.add_view(
534 AdminPermissionsView,
534 AdminPermissionsView,
535 attr='permissions_overview',
535 attr='permissions_overview',
536 route_name='admin_permissions_overview', request_method='GET',
536 route_name='admin_permissions_overview', request_method='GET',
537 renderer='rhodecode:templates/admin/permissions/permissions.mako')
537 renderer='rhodecode:templates/admin/permissions/permissions.mako')
538
538
539 config.add_route(
539 config.add_route(
540 name='admin_permissions_auth_token_access',
540 name='admin_permissions_auth_token_access',
541 pattern='/permissions/auth_token_access')
541 pattern='/permissions/auth_token_access')
542 config.add_view(
542 config.add_view(
543 AdminPermissionsView,
543 AdminPermissionsView,
544 attr='auth_token_access',
544 attr='auth_token_access',
545 route_name='admin_permissions_auth_token_access', request_method='GET',
545 route_name='admin_permissions_auth_token_access', request_method='GET',
546 renderer='rhodecode:templates/admin/permissions/permissions.mako')
546 renderer='rhodecode:templates/admin/permissions/permissions.mako')
547
547
548 config.add_route(
548 config.add_route(
549 name='admin_permissions_ssh_keys',
549 name='admin_permissions_ssh_keys',
550 pattern='/permissions/ssh_keys')
550 pattern='/permissions/ssh_keys')
551 config.add_view(
551 config.add_view(
552 AdminPermissionsView,
552 AdminPermissionsView,
553 attr='ssh_keys',
553 attr='ssh_keys',
554 route_name='admin_permissions_ssh_keys', request_method='GET',
554 route_name='admin_permissions_ssh_keys', request_method='GET',
555 renderer='rhodecode:templates/admin/permissions/permissions.mako')
555 renderer='rhodecode:templates/admin/permissions/permissions.mako')
556
556
557 config.add_route(
557 config.add_route(
558 name='admin_permissions_ssh_keys_data',
558 name='admin_permissions_ssh_keys_data',
559 pattern='/permissions/ssh_keys/data')
559 pattern='/permissions/ssh_keys/data')
560 config.add_view(
560 config.add_view(
561 AdminPermissionsView,
561 AdminPermissionsView,
562 attr='ssh_keys_data',
562 attr='ssh_keys_data',
563 route_name='admin_permissions_ssh_keys_data', request_method='GET',
563 route_name='admin_permissions_ssh_keys_data', request_method='GET',
564 renderer='json_ext', xhr=True)
564 renderer='json_ext', xhr=True)
565
565
566 config.add_route(
566 config.add_route(
567 name='admin_permissions_ssh_keys_update',
567 name='admin_permissions_ssh_keys_update',
568 pattern='/permissions/ssh_keys/update')
568 pattern='/permissions/ssh_keys/update')
569 config.add_view(
569 config.add_view(
570 AdminPermissionsView,
570 AdminPermissionsView,
571 attr='ssh_keys_update',
571 attr='ssh_keys_update',
572 route_name='admin_permissions_ssh_keys_update', request_method='POST',
572 route_name='admin_permissions_ssh_keys_update', request_method='POST',
573 renderer='rhodecode:templates/admin/permissions/permissions.mako')
573 renderer='rhodecode:templates/admin/permissions/permissions.mako')
574
574
575 # users admin
575 # users admin
576 config.add_route(
576 config.add_route(
577 name='users',
577 name='users',
578 pattern='/users')
578 pattern='/users')
579 config.add_view(
579 config.add_view(
580 AdminUsersView,
580 AdminUsersView,
581 attr='users_list',
581 attr='users_list',
582 route_name='users', request_method='GET',
582 route_name='users', request_method='GET',
583 renderer='rhodecode:templates/admin/users/users.mako')
583 renderer='rhodecode:templates/admin/users/users.mako')
584
584
585 config.add_route(
585 config.add_route(
586 name='users_data',
586 name='users_data',
587 pattern='/users_data')
587 pattern='/users_data')
588 config.add_view(
588 config.add_view(
589 AdminUsersView,
589 AdminUsersView,
590 attr='users_list_data',
590 attr='users_list_data',
591 # renderer defined below
591 # renderer defined below
592 route_name='users_data', request_method='GET',
592 route_name='users_data', request_method='GET',
593 renderer='json_ext', xhr=True)
593 renderer='json_ext', xhr=True)
594
594
595 config.add_route(
595 config.add_route(
596 name='users_create',
596 name='users_create',
597 pattern='/users/create')
597 pattern='/users/create')
598 config.add_view(
598 config.add_view(
599 AdminUsersView,
599 AdminUsersView,
600 attr='users_create',
600 attr='users_create',
601 route_name='users_create', request_method='POST',
601 route_name='users_create', request_method='POST',
602 renderer='rhodecode:templates/admin/users/user_add.mako')
602 renderer='rhodecode:templates/admin/users/user_add.mako')
603
603
604 config.add_route(
604 config.add_route(
605 name='users_new',
605 name='users_new',
606 pattern='/users/new')
606 pattern='/users/new')
607 config.add_view(
607 config.add_view(
608 AdminUsersView,
608 AdminUsersView,
609 attr='users_new',
609 attr='users_new',
610 route_name='users_new', request_method='GET',
610 route_name='users_new', request_method='GET',
611 renderer='rhodecode:templates/admin/users/user_add.mako')
611 renderer='rhodecode:templates/admin/users/user_add.mako')
612
612
613 # user management
613 # user management
614 config.add_route(
614 config.add_route(
615 name='user_edit',
615 name='user_edit',
616 pattern='/users/{user_id:\d+}/edit',
616 pattern='/users/{user_id:\d+}/edit',
617 user_route=True)
617 user_route=True)
618 config.add_view(
618 config.add_view(
619 UsersView,
619 UsersView,
620 attr='user_edit',
620 attr='user_edit',
621 route_name='user_edit', request_method='GET',
621 route_name='user_edit', request_method='GET',
622 renderer='rhodecode:templates/admin/users/user_edit.mako')
622 renderer='rhodecode:templates/admin/users/user_edit.mako')
623
623
624 config.add_route(
624 config.add_route(
625 name='user_edit_advanced',
625 name='user_edit_advanced',
626 pattern='/users/{user_id:\d+}/edit/advanced',
626 pattern='/users/{user_id:\d+}/edit/advanced',
627 user_route=True)
627 user_route=True)
628 config.add_view(
628 config.add_view(
629 UsersView,
629 UsersView,
630 attr='user_edit_advanced',
630 attr='user_edit_advanced',
631 route_name='user_edit_advanced', request_method='GET',
631 route_name='user_edit_advanced', request_method='GET',
632 renderer='rhodecode:templates/admin/users/user_edit.mako')
632 renderer='rhodecode:templates/admin/users/user_edit.mako')
633
633
634 config.add_route(
634 config.add_route(
635 name='user_edit_global_perms',
635 name='user_edit_global_perms',
636 pattern='/users/{user_id:\d+}/edit/global_permissions',
636 pattern='/users/{user_id:\d+}/edit/global_permissions',
637 user_route=True)
637 user_route=True)
638 config.add_view(
638 config.add_view(
639 UsersView,
639 UsersView,
640 attr='user_edit_global_perms',
640 attr='user_edit_global_perms',
641 route_name='user_edit_global_perms', request_method='GET',
641 route_name='user_edit_global_perms', request_method='GET',
642 renderer='rhodecode:templates/admin/users/user_edit.mako')
642 renderer='rhodecode:templates/admin/users/user_edit.mako')
643
643
644 config.add_route(
644 config.add_route(
645 name='user_edit_global_perms_update',
645 name='user_edit_global_perms_update',
646 pattern='/users/{user_id:\d+}/edit/global_permissions/update',
646 pattern='/users/{user_id:\d+}/edit/global_permissions/update',
647 user_route=True)
647 user_route=True)
648 config.add_view(
648 config.add_view(
649 UsersView,
649 UsersView,
650 attr='user_edit_global_perms_update',
650 attr='user_edit_global_perms_update',
651 route_name='user_edit_global_perms_update', request_method='POST',
651 route_name='user_edit_global_perms_update', request_method='POST',
652 renderer='rhodecode:templates/admin/users/user_edit.mako')
652 renderer='rhodecode:templates/admin/users/user_edit.mako')
653
653
654 config.add_route(
654 config.add_route(
655 name='user_update',
655 name='user_update',
656 pattern='/users/{user_id:\d+}/update',
656 pattern='/users/{user_id:\d+}/update',
657 user_route=True)
657 user_route=True)
658 config.add_view(
658 config.add_view(
659 UsersView,
659 UsersView,
660 attr='user_update',
660 attr='user_update',
661 route_name='user_update', request_method='POST',
661 route_name='user_update', request_method='POST',
662 renderer='rhodecode:templates/admin/users/user_edit.mako')
662 renderer='rhodecode:templates/admin/users/user_edit.mako')
663
663
664 config.add_route(
664 config.add_route(
665 name='user_delete',
665 name='user_delete',
666 pattern='/users/{user_id:\d+}/delete',
666 pattern='/users/{user_id:\d+}/delete',
667 user_route=True)
667 user_route=True)
668 config.add_view(
668 config.add_view(
669 UsersView,
669 UsersView,
670 attr='user_delete',
670 attr='user_delete',
671 route_name='user_delete', request_method='POST',
671 route_name='user_delete', request_method='POST',
672 renderer='rhodecode:templates/admin/users/user_edit.mako')
672 renderer='rhodecode:templates/admin/users/user_edit.mako')
673
673
674 config.add_route(
674 config.add_route(
675 name='user_enable_force_password_reset',
675 name='user_enable_force_password_reset',
676 pattern='/users/{user_id:\d+}/password_reset_enable',
676 pattern='/users/{user_id:\d+}/password_reset_enable',
677 user_route=True)
677 user_route=True)
678 config.add_view(
678 config.add_view(
679 UsersView,
679 UsersView,
680 attr='user_enable_force_password_reset',
680 attr='user_enable_force_password_reset',
681 route_name='user_enable_force_password_reset', request_method='POST',
681 route_name='user_enable_force_password_reset', request_method='POST',
682 renderer='rhodecode:templates/admin/users/user_edit.mako')
682 renderer='rhodecode:templates/admin/users/user_edit.mako')
683
683
684 config.add_route(
684 config.add_route(
685 name='user_disable_force_password_reset',
685 name='user_disable_force_password_reset',
686 pattern='/users/{user_id:\d+}/password_reset_disable',
686 pattern='/users/{user_id:\d+}/password_reset_disable',
687 user_route=True)
687 user_route=True)
688 config.add_view(
688 config.add_view(
689 UsersView,
689 UsersView,
690 attr='user_disable_force_password_reset',
690 attr='user_disable_force_password_reset',
691 route_name='user_disable_force_password_reset', request_method='POST',
691 route_name='user_disable_force_password_reset', request_method='POST',
692 renderer='rhodecode:templates/admin/users/user_edit.mako')
692 renderer='rhodecode:templates/admin/users/user_edit.mako')
693
693
694 config.add_route(
694 config.add_route(
695 name='user_create_personal_repo_group',
695 name='user_create_personal_repo_group',
696 pattern='/users/{user_id:\d+}/create_repo_group',
696 pattern='/users/{user_id:\d+}/create_repo_group',
697 user_route=True)
697 user_route=True)
698 config.add_view(
698 config.add_view(
699 UsersView,
699 UsersView,
700 attr='user_create_personal_repo_group',
700 attr='user_create_personal_repo_group',
701 route_name='user_create_personal_repo_group', request_method='POST',
701 route_name='user_create_personal_repo_group', request_method='POST',
702 renderer='rhodecode:templates/admin/users/user_edit.mako')
702 renderer='rhodecode:templates/admin/users/user_edit.mako')
703
703
704 # user notice
704 # user notice
705 config.add_route(
705 config.add_route(
706 name='user_notice_dismiss',
706 name='user_notice_dismiss',
707 pattern='/users/{user_id:\d+}/notice_dismiss',
707 pattern='/users/{user_id:\d+}/notice_dismiss',
708 user_route=True)
708 user_route=True)
709 config.add_view(
709 config.add_view(
710 UsersView,
710 UsersView,
711 attr='user_notice_dismiss',
711 attr='user_notice_dismiss',
712 route_name='user_notice_dismiss', request_method='POST',
712 route_name='user_notice_dismiss', request_method='POST',
713 renderer='json_ext', xhr=True)
713 renderer='json_ext', xhr=True)
714
714
715 # user auth tokens
715 # user auth tokens
716 config.add_route(
716 config.add_route(
717 name='edit_user_auth_tokens',
717 name='edit_user_auth_tokens',
718 pattern='/users/{user_id:\d+}/edit/auth_tokens',
718 pattern='/users/{user_id:\d+}/edit/auth_tokens',
719 user_route=True)
719 user_route=True)
720 config.add_view(
720 config.add_view(
721 UsersView,
721 UsersView,
722 attr='auth_tokens',
722 attr='auth_tokens',
723 route_name='edit_user_auth_tokens', request_method='GET',
723 route_name='edit_user_auth_tokens', request_method='GET',
724 renderer='rhodecode:templates/admin/users/user_edit.mako')
724 renderer='rhodecode:templates/admin/users/user_edit.mako')
725
725
726 config.add_route(
726 config.add_route(
727 name='edit_user_auth_tokens_view',
727 name='edit_user_auth_tokens_view',
728 pattern='/users/{user_id:\d+}/edit/auth_tokens/view',
728 pattern='/users/{user_id:\d+}/edit/auth_tokens/view',
729 user_route=True)
729 user_route=True)
730 config.add_view(
730 config.add_view(
731 UsersView,
731 UsersView,
732 attr='auth_tokens_view',
732 attr='auth_tokens_view',
733 route_name='edit_user_auth_tokens_view', request_method='POST',
733 route_name='edit_user_auth_tokens_view', request_method='POST',
734 renderer='json_ext', xhr=True)
734 renderer='json_ext', xhr=True)
735
735
736 config.add_route(
736 config.add_route(
737 name='edit_user_auth_tokens_add',
737 name='edit_user_auth_tokens_add',
738 pattern='/users/{user_id:\d+}/edit/auth_tokens/new',
738 pattern='/users/{user_id:\d+}/edit/auth_tokens/new',
739 user_route=True)
739 user_route=True)
740 config.add_view(
740 config.add_view(
741 UsersView,
741 UsersView,
742 attr='auth_tokens_add',
742 attr='auth_tokens_add',
743 route_name='edit_user_auth_tokens_add', request_method='POST')
743 route_name='edit_user_auth_tokens_add', request_method='POST')
744
744
745 config.add_route(
745 config.add_route(
746 name='edit_user_auth_tokens_delete',
746 name='edit_user_auth_tokens_delete',
747 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete',
747 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete',
748 user_route=True)
748 user_route=True)
749 config.add_view(
749 config.add_view(
750 UsersView,
750 UsersView,
751 attr='auth_tokens_delete',
751 attr='auth_tokens_delete',
752 route_name='edit_user_auth_tokens_delete', request_method='POST')
752 route_name='edit_user_auth_tokens_delete', request_method='POST')
753
753
754 # user ssh keys
754 # user ssh keys
755 config.add_route(
755 config.add_route(
756 name='edit_user_ssh_keys',
756 name='edit_user_ssh_keys',
757 pattern='/users/{user_id:\d+}/edit/ssh_keys',
757 pattern='/users/{user_id:\d+}/edit/ssh_keys',
758 user_route=True)
758 user_route=True)
759 config.add_view(
759 config.add_view(
760 UsersView,
760 UsersView,
761 attr='ssh_keys',
761 attr='ssh_keys',
762 route_name='edit_user_ssh_keys', request_method='GET',
762 route_name='edit_user_ssh_keys', request_method='GET',
763 renderer='rhodecode:templates/admin/users/user_edit.mako')
763 renderer='rhodecode:templates/admin/users/user_edit.mako')
764
764
765 config.add_route(
765 config.add_route(
766 name='edit_user_ssh_keys_generate_keypair',
766 name='edit_user_ssh_keys_generate_keypair',
767 pattern='/users/{user_id:\d+}/edit/ssh_keys/generate',
767 pattern='/users/{user_id:\d+}/edit/ssh_keys/generate',
768 user_route=True)
768 user_route=True)
769 config.add_view(
769 config.add_view(
770 UsersView,
770 UsersView,
771 attr='ssh_keys_generate_keypair',
771 attr='ssh_keys_generate_keypair',
772 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
772 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
773 renderer='rhodecode:templates/admin/users/user_edit.mako')
773 renderer='rhodecode:templates/admin/users/user_edit.mako')
774
774
775 config.add_route(
775 config.add_route(
776 name='edit_user_ssh_keys_add',
776 name='edit_user_ssh_keys_add',
777 pattern='/users/{user_id:\d+}/edit/ssh_keys/new',
777 pattern='/users/{user_id:\d+}/edit/ssh_keys/new',
778 user_route=True)
778 user_route=True)
779 config.add_view(
779 config.add_view(
780 UsersView,
780 UsersView,
781 attr='ssh_keys_add',
781 attr='ssh_keys_add',
782 route_name='edit_user_ssh_keys_add', request_method='POST')
782 route_name='edit_user_ssh_keys_add', request_method='POST')
783
783
784 config.add_route(
784 config.add_route(
785 name='edit_user_ssh_keys_delete',
785 name='edit_user_ssh_keys_delete',
786 pattern='/users/{user_id:\d+}/edit/ssh_keys/delete',
786 pattern='/users/{user_id:\d+}/edit/ssh_keys/delete',
787 user_route=True)
787 user_route=True)
788 config.add_view(
788 config.add_view(
789 UsersView,
789 UsersView,
790 attr='ssh_keys_delete',
790 attr='ssh_keys_delete',
791 route_name='edit_user_ssh_keys_delete', request_method='POST')
791 route_name='edit_user_ssh_keys_delete', request_method='POST')
792
792
793 # user emails
793 # user emails
794 config.add_route(
794 config.add_route(
795 name='edit_user_emails',
795 name='edit_user_emails',
796 pattern='/users/{user_id:\d+}/edit/emails',
796 pattern='/users/{user_id:\d+}/edit/emails',
797 user_route=True)
797 user_route=True)
798 config.add_view(
798 config.add_view(
799 UsersView,
799 UsersView,
800 attr='emails',
800 attr='emails',
801 route_name='edit_user_emails', request_method='GET',
801 route_name='edit_user_emails', request_method='GET',
802 renderer='rhodecode:templates/admin/users/user_edit.mako')
802 renderer='rhodecode:templates/admin/users/user_edit.mako')
803
803
804 config.add_route(
804 config.add_route(
805 name='edit_user_emails_add',
805 name='edit_user_emails_add',
806 pattern='/users/{user_id:\d+}/edit/emails/new',
806 pattern='/users/{user_id:\d+}/edit/emails/new',
807 user_route=True)
807 user_route=True)
808 config.add_view(
808 config.add_view(
809 UsersView,
809 UsersView,
810 attr='emails_add',
810 attr='emails_add',
811 route_name='edit_user_emails_add', request_method='POST')
811 route_name='edit_user_emails_add', request_method='POST')
812
812
813 config.add_route(
813 config.add_route(
814 name='edit_user_emails_delete',
814 name='edit_user_emails_delete',
815 pattern='/users/{user_id:\d+}/edit/emails/delete',
815 pattern='/users/{user_id:\d+}/edit/emails/delete',
816 user_route=True)
816 user_route=True)
817 config.add_view(
817 config.add_view(
818 UsersView,
818 UsersView,
819 attr='emails_delete',
819 attr='emails_delete',
820 route_name='edit_user_emails_delete', request_method='POST')
820 route_name='edit_user_emails_delete', request_method='POST')
821
821
822 # user IPs
822 # user IPs
823 config.add_route(
823 config.add_route(
824 name='edit_user_ips',
824 name='edit_user_ips',
825 pattern='/users/{user_id:\d+}/edit/ips',
825 pattern='/users/{user_id:\d+}/edit/ips',
826 user_route=True)
826 user_route=True)
827 config.add_view(
827 config.add_view(
828 UsersView,
828 UsersView,
829 attr='ips',
829 attr='ips',
830 route_name='edit_user_ips', request_method='GET',
830 route_name='edit_user_ips', request_method='GET',
831 renderer='rhodecode:templates/admin/users/user_edit.mako')
831 renderer='rhodecode:templates/admin/users/user_edit.mako')
832
832
833 config.add_route(
833 config.add_route(
834 name='edit_user_ips_add',
834 name='edit_user_ips_add',
835 pattern='/users/{user_id:\d+}/edit/ips/new',
835 pattern='/users/{user_id:\d+}/edit/ips/new',
836 user_route_with_default=True) # enabled for default user too
836 user_route_with_default=True) # enabled for default user too
837 config.add_view(
837 config.add_view(
838 UsersView,
838 UsersView,
839 attr='ips_add',
839 attr='ips_add',
840 route_name='edit_user_ips_add', request_method='POST')
840 route_name='edit_user_ips_add', request_method='POST')
841
841
842 config.add_route(
842 config.add_route(
843 name='edit_user_ips_delete',
843 name='edit_user_ips_delete',
844 pattern='/users/{user_id:\d+}/edit/ips/delete',
844 pattern='/users/{user_id:\d+}/edit/ips/delete',
845 user_route_with_default=True) # enabled for default user too
845 user_route_with_default=True) # enabled for default user too
846 config.add_view(
846 config.add_view(
847 UsersView,
847 UsersView,
848 attr='ips_delete',
848 attr='ips_delete',
849 route_name='edit_user_ips_delete', request_method='POST')
849 route_name='edit_user_ips_delete', request_method='POST')
850
850
851 # user perms
851 # user perms
852 config.add_route(
852 config.add_route(
853 name='edit_user_perms_summary',
853 name='edit_user_perms_summary',
854 pattern='/users/{user_id:\d+}/edit/permissions_summary',
854 pattern='/users/{user_id:\d+}/edit/permissions_summary',
855 user_route=True)
855 user_route=True)
856 config.add_view(
856 config.add_view(
857 UsersView,
857 UsersView,
858 attr='user_perms_summary',
858 attr='user_perms_summary',
859 route_name='edit_user_perms_summary', request_method='GET',
859 route_name='edit_user_perms_summary', request_method='GET',
860 renderer='rhodecode:templates/admin/users/user_edit.mako')
860 renderer='rhodecode:templates/admin/users/user_edit.mako')
861
861
862 config.add_route(
862 config.add_route(
863 name='edit_user_perms_summary_json',
863 name='edit_user_perms_summary_json',
864 pattern='/users/{user_id:\d+}/edit/permissions_summary/json',
864 pattern='/users/{user_id:\d+}/edit/permissions_summary/json',
865 user_route=True)
865 user_route=True)
866 config.add_view(
866 config.add_view(
867 UsersView,
867 UsersView,
868 attr='user_perms_summary_json',
868 attr='user_perms_summary_json',
869 route_name='edit_user_perms_summary_json', request_method='GET',
869 route_name='edit_user_perms_summary_json', request_method='GET',
870 renderer='json_ext')
870 renderer='json_ext')
871
871
872 # user user groups management
872 # user user groups management
873 config.add_route(
873 config.add_route(
874 name='edit_user_groups_management',
874 name='edit_user_groups_management',
875 pattern='/users/{user_id:\d+}/edit/groups_management',
875 pattern='/users/{user_id:\d+}/edit/groups_management',
876 user_route=True)
876 user_route=True)
877 config.add_view(
877 config.add_view(
878 UsersView,
878 UsersView,
879 attr='groups_management',
879 attr='groups_management',
880 route_name='edit_user_groups_management', request_method='GET',
880 route_name='edit_user_groups_management', request_method='GET',
881 renderer='rhodecode:templates/admin/users/user_edit.mako')
881 renderer='rhodecode:templates/admin/users/user_edit.mako')
882
882
883 config.add_route(
883 config.add_route(
884 name='edit_user_groups_management_updates',
884 name='edit_user_groups_management_updates',
885 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
885 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
886 user_route=True)
886 user_route=True)
887 config.add_view(
887 config.add_view(
888 UsersView,
888 UsersView,
889 attr='groups_management_updates',
889 attr='groups_management_updates',
890 route_name='edit_user_groups_management_updates', request_method='POST')
890 route_name='edit_user_groups_management_updates', request_method='POST')
891
891
892 # user audit logs
892 # user audit logs
893 config.add_route(
893 config.add_route(
894 name='edit_user_audit_logs',
894 name='edit_user_audit_logs',
895 pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
895 pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
896 config.add_view(
896 config.add_view(
897 UsersView,
897 UsersView,
898 attr='user_audit_logs',
898 attr='user_audit_logs',
899 route_name='edit_user_audit_logs', request_method='GET',
899 route_name='edit_user_audit_logs', request_method='GET',
900 renderer='rhodecode:templates/admin/users/user_edit.mako')
900 renderer='rhodecode:templates/admin/users/user_edit.mako')
901
901
902 config.add_route(
902 config.add_route(
903 name='edit_user_audit_logs_download',
903 name='edit_user_audit_logs_download',
904 pattern='/users/{user_id:\d+}/edit/audit/download', user_route=True)
904 pattern='/users/{user_id:\d+}/edit/audit/download', user_route=True)
905 config.add_view(
905 config.add_view(
906 UsersView,
906 UsersView,
907 attr='user_audit_logs_download',
907 attr='user_audit_logs_download',
908 route_name='edit_user_audit_logs_download', request_method='GET',
908 route_name='edit_user_audit_logs_download', request_method='GET',
909 renderer='string')
909 renderer='string')
910
910
911 # user caches
911 # user caches
912 config.add_route(
912 config.add_route(
913 name='edit_user_caches',
913 name='edit_user_caches',
914 pattern='/users/{user_id:\d+}/edit/caches',
914 pattern='/users/{user_id:\d+}/edit/caches',
915 user_route=True)
915 user_route=True)
916 config.add_view(
916 config.add_view(
917 UsersView,
917 UsersView,
918 attr='user_caches',
918 attr='user_caches',
919 route_name='edit_user_caches', request_method='GET',
919 route_name='edit_user_caches', request_method='GET',
920 renderer='rhodecode:templates/admin/users/user_edit.mako')
920 renderer='rhodecode:templates/admin/users/user_edit.mako')
921
921
922 config.add_route(
922 config.add_route(
923 name='edit_user_caches_update',
923 name='edit_user_caches_update',
924 pattern='/users/{user_id:\d+}/edit/caches/update',
924 pattern='/users/{user_id:\d+}/edit/caches/update',
925 user_route=True)
925 user_route=True)
926 config.add_view(
926 config.add_view(
927 UsersView,
927 UsersView,
928 attr='user_caches_update',
928 attr='user_caches_update',
929 route_name='edit_user_caches_update', request_method='POST')
929 route_name='edit_user_caches_update', request_method='POST')
930
930
931 # user-groups admin
931 # user-groups admin
932 config.add_route(
932 config.add_route(
933 name='user_groups',
933 name='user_groups',
934 pattern='/user_groups')
934 pattern='/user_groups')
935 config.add_view(
935 config.add_view(
936 AdminUserGroupsView,
936 AdminUserGroupsView,
937 attr='user_groups_list',
937 attr='user_groups_list',
938 route_name='user_groups', request_method='GET',
938 route_name='user_groups', request_method='GET',
939 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
939 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
940
940
941 config.add_route(
941 config.add_route(
942 name='user_groups_data',
942 name='user_groups_data',
943 pattern='/user_groups_data')
943 pattern='/user_groups_data')
944 config.add_view(
944 config.add_view(
945 AdminUserGroupsView,
945 AdminUserGroupsView,
946 attr='user_groups_list_data',
946 attr='user_groups_list_data',
947 route_name='user_groups_data', request_method='GET',
947 route_name='user_groups_data', request_method='GET',
948 renderer='json_ext', xhr=True)
948 renderer='json_ext', xhr=True)
949
949
950 config.add_route(
950 config.add_route(
951 name='user_groups_new',
951 name='user_groups_new',
952 pattern='/user_groups/new')
952 pattern='/user_groups/new')
953 config.add_view(
953 config.add_view(
954 AdminUserGroupsView,
954 AdminUserGroupsView,
955 attr='user_groups_new',
955 attr='user_groups_new',
956 route_name='user_groups_new', request_method='GET',
956 route_name='user_groups_new', request_method='GET',
957 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
957 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
958
958
959 config.add_route(
959 config.add_route(
960 name='user_groups_create',
960 name='user_groups_create',
961 pattern='/user_groups/create')
961 pattern='/user_groups/create')
962 config.add_view(
962 config.add_view(
963 AdminUserGroupsView,
963 AdminUserGroupsView,
964 attr='user_groups_create',
964 attr='user_groups_create',
965 route_name='user_groups_create', request_method='POST',
965 route_name='user_groups_create', request_method='POST',
966 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
966 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
967
967
968 # repos admin
968 # repos admin
969 config.add_route(
969 config.add_route(
970 name='repos',
970 name='repos',
971 pattern='/repos')
971 pattern='/repos')
972 config.add_view(
972 config.add_view(
973 AdminReposView,
973 AdminReposView,
974 attr='repository_list',
974 attr='repository_list',
975 route_name='repos', request_method='GET',
975 route_name='repos', request_method='GET',
976 renderer='rhodecode:templates/admin/repos/repos.mako')
976 renderer='rhodecode:templates/admin/repos/repos.mako')
977
977
978 config.add_route(
978 config.add_route(
979 name='repos_data',
979 name='repos_data',
980 pattern='/repos_data')
980 pattern='/repos_data')
981 config.add_view(
981 config.add_view(
982 AdminReposView,
982 AdminReposView,
983 attr='repository_list_data',
983 attr='repository_list_data',
984 route_name='repos_data', request_method='GET',
984 route_name='repos_data', request_method='GET',
985 renderer='json_ext', xhr=True)
985 renderer='json_ext', xhr=True)
986
986
987 config.add_route(
987 config.add_route(
988 name='repo_new',
988 name='repo_new',
989 pattern='/repos/new')
989 pattern='/repos/new')
990 config.add_view(
990 config.add_view(
991 AdminReposView,
991 AdminReposView,
992 attr='repository_new',
992 attr='repository_new',
993 route_name='repo_new', request_method='GET',
993 route_name='repo_new', request_method='GET',
994 renderer='rhodecode:templates/admin/repos/repo_add.mako')
994 renderer='rhodecode:templates/admin/repos/repo_add.mako')
995
995
996 config.add_route(
996 config.add_route(
997 name='repo_create',
997 name='repo_create',
998 pattern='/repos/create')
998 pattern='/repos/create')
999 config.add_view(
999 config.add_view(
1000 AdminReposView,
1000 AdminReposView,
1001 attr='repository_create',
1001 attr='repository_create',
1002 route_name='repo_create', request_method='POST',
1002 route_name='repo_create', request_method='POST',
1003 renderer='rhodecode:templates/admin/repos/repos.mako')
1003 renderer='rhodecode:templates/admin/repos/repos.mako')
1004
1004
1005 # repo groups admin
1005 # repo groups admin
1006 config.add_route(
1006 config.add_route(
1007 name='repo_groups',
1007 name='repo_groups',
1008 pattern='/repo_groups')
1008 pattern='/repo_groups')
1009 config.add_view(
1009 config.add_view(
1010 AdminRepoGroupsView,
1010 AdminRepoGroupsView,
1011 attr='repo_group_list',
1011 attr='repo_group_list',
1012 route_name='repo_groups', request_method='GET',
1012 route_name='repo_groups', request_method='GET',
1013 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
1013 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
1014
1014
1015 config.add_route(
1015 config.add_route(
1016 name='repo_groups_data',
1016 name='repo_groups_data',
1017 pattern='/repo_groups_data')
1017 pattern='/repo_groups_data')
1018 config.add_view(
1018 config.add_view(
1019 AdminRepoGroupsView,
1019 AdminRepoGroupsView,
1020 attr='repo_group_list_data',
1020 attr='repo_group_list_data',
1021 route_name='repo_groups_data', request_method='GET',
1021 route_name='repo_groups_data', request_method='GET',
1022 renderer='json_ext', xhr=True)
1022 renderer='json_ext', xhr=True)
1023
1023
1024 config.add_route(
1024 config.add_route(
1025 name='repo_group_new',
1025 name='repo_group_new',
1026 pattern='/repo_group/new')
1026 pattern='/repo_group/new')
1027 config.add_view(
1027 config.add_view(
1028 AdminRepoGroupsView,
1028 AdminRepoGroupsView,
1029 attr='repo_group_new',
1029 attr='repo_group_new',
1030 route_name='repo_group_new', request_method='GET',
1030 route_name='repo_group_new', request_method='GET',
1031 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1031 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1032
1032
1033 config.add_route(
1033 config.add_route(
1034 name='repo_group_create',
1034 name='repo_group_create',
1035 pattern='/repo_group/create')
1035 pattern='/repo_group/create')
1036 config.add_view(
1036 config.add_view(
1037 AdminRepoGroupsView,
1037 AdminRepoGroupsView,
1038 attr='repo_group_create',
1038 attr='repo_group_create',
1039 route_name='repo_group_create', request_method='POST',
1039 route_name='repo_group_create', request_method='POST',
1040 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1040 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
1041
1041
1042
1042
1043 def includeme(config):
1043 def includeme(config):
1044 from rhodecode.apps._base.navigation import includeme as nav_includeme
1044 from rhodecode.apps._base.navigation import includeme as nav_includeme
1045 from rhodecode.apps.admin.views.main_views import AdminMainView
1045 from rhodecode.apps.admin.views.main_views import AdminMainView
1046
1046
1047 # Create admin navigation registry and add it to the pyramid registry.
1047 # Create admin navigation registry and add it to the pyramid registry.
1048 nav_includeme(config)
1048 nav_includeme(config)
1049
1049
1050 # main admin routes
1050 # main admin routes
1051 config.add_route(
1051 config.add_route(
1052 name='admin_home', pattern=ADMIN_PREFIX)
1052 name='admin_home', pattern=ADMIN_PREFIX)
1053 config.add_view(
1053 config.add_view(
1054 AdminMainView,
1054 AdminMainView,
1055 attr='admin_main',
1055 attr='admin_main',
1056 route_name='admin_home', request_method='GET',
1056 route_name='admin_home', request_method='GET',
1057 renderer='rhodecode:templates/admin/main.mako')
1057 renderer='rhodecode:templates/admin/main.mako')
1058
1058
1059 # pr global redirect
1059 # pr global redirect
1060 config.add_route(
1060 config.add_route(
1061 name='pull_requests_global_0', # backward compat
1061 name='pull_requests_global_0', # backward compat
1062 pattern=ADMIN_PREFIX + '/pull_requests/{pull_request_id:\d+}')
1062 pattern=ADMIN_PREFIX + '/pull_requests/{pull_request_id:\d+}')
1063 config.add_view(
1063 config.add_view(
1064 AdminMainView,
1064 AdminMainView,
1065 attr='pull_requests',
1065 attr='pull_requests',
1066 route_name='pull_requests_global_0', request_method='GET')
1066 route_name='pull_requests_global_0', request_method='GET')
1067
1067
1068 config.add_route(
1068 config.add_route(
1069 name='pull_requests_global_1', # backward compat
1069 name='pull_requests_global_1', # backward compat
1070 pattern=ADMIN_PREFIX + '/pull-requests/{pull_request_id:\d+}')
1070 pattern=ADMIN_PREFIX + '/pull-requests/{pull_request_id:\d+}')
1071 config.add_view(
1071 config.add_view(
1072 AdminMainView,
1072 AdminMainView,
1073 attr='pull_requests',
1073 attr='pull_requests',
1074 route_name='pull_requests_global_1', request_method='GET')
1074 route_name='pull_requests_global_1', request_method='GET')
1075
1075
1076 config.add_route(
1076 config.add_route(
1077 name='pull_requests_global',
1077 name='pull_requests_global',
1078 pattern=ADMIN_PREFIX + '/pull-request/{pull_request_id:\d+}')
1078 pattern=ADMIN_PREFIX + '/pull-request/{pull_request_id:\d+}')
1079 config.add_view(
1079 config.add_view(
1080 AdminMainView,
1080 AdminMainView,
1081 attr='pull_requests',
1081 attr='pull_requests',
1082 route_name='pull_requests_global', request_method='GET')
1082 route_name='pull_requests_global', request_method='GET')
1083
1083
1084 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
1084 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,171 +1,170 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import os
20 import os
22 import csv
21 import csv
23 import datetime
22 import datetime
24
23
25 import pytest
24 import pytest
26
25
27 from rhodecode.tests import *
26 from rhodecode.tests import *
28 from rhodecode.tests.fixture import FIXTURES
27 from rhodecode.tests.fixture import FIXTURES
29 from rhodecode.model.db import UserLog
28 from rhodecode.model.db import UserLog
30 from rhodecode.model.meta import Session
29 from rhodecode.model.meta import Session
31 from rhodecode.lib.utils2 import safe_unicode
30 from rhodecode.lib.utils2 import safe_unicode
32
31
33
32
34 def route_path(name, params=None, **kwargs):
33 def route_path(name, params=None, **kwargs):
35 import urllib.request, urllib.parse, urllib.error
34 import urllib.request, urllib.parse, urllib.error
36 from rhodecode.apps._base import ADMIN_PREFIX
35 from rhodecode.apps._base import ADMIN_PREFIX
37
36
38 base_url = {
37 base_url = {
39 'admin_home': ADMIN_PREFIX,
38 'admin_home': ADMIN_PREFIX,
40 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
39 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
41
40
42 }[name].format(**kwargs)
41 }[name].format(**kwargs)
43
42
44 if params:
43 if params:
45 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 return base_url
45 return base_url
47
46
48
47
49 @pytest.mark.usefixtures('app')
48 @pytest.mark.usefixtures('app')
50 class TestAdminController(object):
49 class TestAdminController(object):
51
50
52 @pytest.fixture(scope='class', autouse=True)
51 @pytest.fixture(scope='class', autouse=True)
53 def prepare(self, request, baseapp):
52 def prepare(self, request, baseapp):
54 UserLog.query().delete()
53 UserLog.query().delete()
55 Session().commit()
54 Session().commit()
56
55
57 def strptime(val):
56 def strptime(val):
58 fmt = '%Y-%m-%d %H:%M:%S'
57 fmt = '%Y-%m-%d %H:%M:%S'
59 if '.' not in val:
58 if '.' not in val:
60 return datetime.datetime.strptime(val, fmt)
59 return datetime.datetime.strptime(val, fmt)
61
60
62 nofrag, frag = val.split(".")
61 nofrag, frag = val.split(".")
63 date = datetime.datetime.strptime(nofrag, fmt)
62 date = datetime.datetime.strptime(nofrag, fmt)
64
63
65 frag = frag[:6] # truncate to microseconds
64 frag = frag[:6] # truncate to microseconds
66 frag += (6 - len(frag)) * '0' # add 0s
65 frag += (6 - len(frag)) * '0' # add 0s
67 return date.replace(microsecond=int(frag))
66 return date.replace(microsecond=int(frag))
68
67
69 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
68 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
70 for row in csv.DictReader(f):
69 for row in csv.DictReader(f):
71 ul = UserLog()
70 ul = UserLog()
72 for k, v in row.items():
71 for k, v in row.items():
73 v = safe_unicode(v)
72 v = safe_unicode(v)
74 if k == 'action_date':
73 if k == 'action_date':
75 v = strptime(v)
74 v = strptime(v)
76 if k in ['user_id', 'repository_id']:
75 if k in ['user_id', 'repository_id']:
77 # nullable due to FK problems
76 # nullable due to FK problems
78 v = None
77 v = None
79 setattr(ul, k, v)
78 setattr(ul, k, v)
80 Session().add(ul)
79 Session().add(ul)
81 Session().commit()
80 Session().commit()
82
81
83 @request.addfinalizer
82 @request.addfinalizer
84 def cleanup():
83 def cleanup():
85 UserLog.query().delete()
84 UserLog.query().delete()
86 Session().commit()
85 Session().commit()
87
86
88 def test_index(self, autologin_user):
87 def test_index(self, autologin_user):
89 response = self.app.get(route_path('admin_audit_logs'))
88 response = self.app.get(route_path('admin_audit_logs'))
90 response.mustcontain('Admin audit logs')
89 response.mustcontain('Admin audit logs')
91
90
92 def test_filter_all_entries(self, autologin_user):
91 def test_filter_all_entries(self, autologin_user):
93 response = self.app.get(route_path('admin_audit_logs'))
92 response = self.app.get(route_path('admin_audit_logs'))
94 all_count = UserLog.query().count()
93 all_count = UserLog.query().count()
95 response.mustcontain('%s entries' % all_count)
94 response.mustcontain('%s entries' % all_count)
96
95
97 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
96 def test_filter_journal_filter_exact_match_on_repository(self, autologin_user):
98 response = self.app.get(route_path('admin_audit_logs',
97 response = self.app.get(route_path('admin_audit_logs',
99 params=dict(filter='repository:rhodecode')))
98 params=dict(filter='repository:rhodecode')))
100 response.mustcontain('3 entries')
99 response.mustcontain('3 entries')
101
100
102 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
101 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self, autologin_user):
103 response = self.app.get(route_path('admin_audit_logs',
102 response = self.app.get(route_path('admin_audit_logs',
104 params=dict(filter='repository:RhodeCode')))
103 params=dict(filter='repository:RhodeCode')))
105 response.mustcontain('3 entries')
104 response.mustcontain('3 entries')
106
105
107 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
106 def test_filter_journal_filter_wildcard_on_repository(self, autologin_user):
108 response = self.app.get(route_path('admin_audit_logs',
107 response = self.app.get(route_path('admin_audit_logs',
109 params=dict(filter='repository:*test*')))
108 params=dict(filter='repository:*test*')))
110 response.mustcontain('862 entries')
109 response.mustcontain('862 entries')
111
110
112 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
111 def test_filter_journal_filter_prefix_on_repository(self, autologin_user):
113 response = self.app.get(route_path('admin_audit_logs',
112 response = self.app.get(route_path('admin_audit_logs',
114 params=dict(filter='repository:test*')))
113 params=dict(filter='repository:test*')))
115 response.mustcontain('257 entries')
114 response.mustcontain('257 entries')
116
115
117 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
116 def test_filter_journal_filter_prefix_on_repository_CamelCase(self, autologin_user):
118 response = self.app.get(route_path('admin_audit_logs',
117 response = self.app.get(route_path('admin_audit_logs',
119 params=dict(filter='repository:Test*')))
118 params=dict(filter='repository:Test*')))
120 response.mustcontain('257 entries')
119 response.mustcontain('257 entries')
121
120
122 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
121 def test_filter_journal_filter_prefix_on_repository_and_user(self, autologin_user):
123 response = self.app.get(route_path('admin_audit_logs',
122 response = self.app.get(route_path('admin_audit_logs',
124 params=dict(filter='repository:test* AND username:demo')))
123 params=dict(filter='repository:test* AND username:demo')))
125 response.mustcontain('130 entries')
124 response.mustcontain('130 entries')
126
125
127 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
126 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self, autologin_user):
128 response = self.app.get(route_path('admin_audit_logs',
127 response = self.app.get(route_path('admin_audit_logs',
129 params=dict(filter='repository:test* OR repository:rhodecode')))
128 params=dict(filter='repository:test* OR repository:rhodecode')))
130 response.mustcontain('260 entries') # 257 + 3
129 response.mustcontain('260 entries') # 257 + 3
131
130
132 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
131 def test_filter_journal_filter_exact_match_on_username(self, autologin_user):
133 response = self.app.get(route_path('admin_audit_logs',
132 response = self.app.get(route_path('admin_audit_logs',
134 params=dict(filter='username:demo')))
133 params=dict(filter='username:demo')))
135 response.mustcontain('1087 entries')
134 response.mustcontain('1087 entries')
136
135
137 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
136 def test_filter_journal_filter_exact_match_on_username_camelCase(self, autologin_user):
138 response = self.app.get(route_path('admin_audit_logs',
137 response = self.app.get(route_path('admin_audit_logs',
139 params=dict(filter='username:DemO')))
138 params=dict(filter='username:DemO')))
140 response.mustcontain('1087 entries')
139 response.mustcontain('1087 entries')
141
140
142 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
141 def test_filter_journal_filter_wildcard_on_username(self, autologin_user):
143 response = self.app.get(route_path('admin_audit_logs',
142 response = self.app.get(route_path('admin_audit_logs',
144 params=dict(filter='username:*test*')))
143 params=dict(filter='username:*test*')))
145 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
144 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
146 response.mustcontain('{} entries'.format(entries_count))
145 response.mustcontain('{} entries'.format(entries_count))
147
146
148 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
147 def test_filter_journal_filter_prefix_on_username(self, autologin_user):
149 response = self.app.get(route_path('admin_audit_logs',
148 response = self.app.get(route_path('admin_audit_logs',
150 params=dict(filter='username:demo*')))
149 params=dict(filter='username:demo*')))
151 response.mustcontain('1101 entries')
150 response.mustcontain('1101 entries')
152
151
153 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
152 def test_filter_journal_filter_prefix_on_user_or_other_user(self, autologin_user):
154 response = self.app.get(route_path('admin_audit_logs',
153 response = self.app.get(route_path('admin_audit_logs',
155 params=dict(filter='username:demo OR username:volcan')))
154 params=dict(filter='username:demo OR username:volcan')))
156 response.mustcontain('1095 entries') # 1087 + 8
155 response.mustcontain('1095 entries') # 1087 + 8
157
156
158 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
157 def test_filter_journal_filter_wildcard_on_action(self, autologin_user):
159 response = self.app.get(route_path('admin_audit_logs',
158 response = self.app.get(route_path('admin_audit_logs',
160 params=dict(filter='action:*pull_request*')))
159 params=dict(filter='action:*pull_request*')))
161 response.mustcontain('187 entries')
160 response.mustcontain('187 entries')
162
161
163 def test_filter_journal_filter_on_date(self, autologin_user):
162 def test_filter_journal_filter_on_date(self, autologin_user):
164 response = self.app.get(route_path('admin_audit_logs',
163 response = self.app.get(route_path('admin_audit_logs',
165 params=dict(filter='date:20121010')))
164 params=dict(filter='date:20121010')))
166 response.mustcontain('47 entries')
165 response.mustcontain('47 entries')
167
166
168 def test_filter_journal_filter_on_date_2(self, autologin_user):
167 def test_filter_journal_filter_on_date_2(self, autologin_user):
169 response = self.app.get(route_path('admin_audit_logs',
168 response = self.app.get(route_path('admin_audit_logs',
170 params=dict(filter='date:20121020')))
169 params=dict(filter='date:20121020')))
171 response.mustcontain('17 entries')
170 response.mustcontain('17 entries')
@@ -1,202 +1,201 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22
21
23 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
24 from rhodecode.tests.utils import AssertResponse
23 from rhodecode.tests.utils import AssertResponse
25 from rhodecode.model.db import Session
24 from rhodecode.model.db import Session
26 from rhodecode.model.settings import SettingsModel
25 from rhodecode.model.settings import SettingsModel
27
26
28
27
29 def assert_auth_settings_updated(response):
28 def assert_auth_settings_updated(response):
30 assert response.status_int == 302, 'Expected response HTTP Found 302'
29 assert response.status_int == 302, 'Expected response HTTP Found 302'
31 assert_session_flash(response, 'Auth settings updated successfully')
30 assert_session_flash(response, 'Auth settings updated successfully')
32
31
33
32
34 @pytest.mark.usefixtures("autologin_user", "app")
33 @pytest.mark.usefixtures("autologin_user", "app")
35 class TestAuthSettingsView(object):
34 class TestAuthSettingsView(object):
36
35
37 def _enable_plugins(self, plugins_list, csrf_token, override=None,
36 def _enable_plugins(self, plugins_list, csrf_token, override=None,
38 verify_response=False):
37 verify_response=False):
39 test_url = '/_admin/auth'
38 test_url = '/_admin/auth'
40 params = {
39 params = {
41 'auth_plugins': plugins_list,
40 'auth_plugins': plugins_list,
42 'csrf_token': csrf_token,
41 'csrf_token': csrf_token,
43 }
42 }
44 if override:
43 if override:
45 params.update(override)
44 params.update(override)
46 _enabled_plugins = []
45 _enabled_plugins = []
47 for plugin in plugins_list.split(','):
46 for plugin in plugins_list.split(','):
48 plugin_name = plugin.partition('#')[-1]
47 plugin_name = plugin.partition('#')[-1]
49 enabled_plugin = '%s_enabled' % plugin_name
48 enabled_plugin = '%s_enabled' % plugin_name
50 cache_ttl = '%s_cache_ttl' % plugin_name
49 cache_ttl = '%s_cache_ttl' % plugin_name
51
50
52 # default params that are needed for each plugin,
51 # default params that are needed for each plugin,
53 # `enabled` and `cache_ttl`
52 # `enabled` and `cache_ttl`
54 params.update({
53 params.update({
55 enabled_plugin: True,
54 enabled_plugin: True,
56 cache_ttl: 0
55 cache_ttl: 0
57 })
56 })
58 _enabled_plugins.append(enabled_plugin)
57 _enabled_plugins.append(enabled_plugin)
59
58
60 # we need to clean any enabled plugin before, since they require
59 # we need to clean any enabled plugin before, since they require
61 # form params to be present
60 # form params to be present
62 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
61 db_plugin = SettingsModel().get_setting_by_name('auth_plugins')
63 db_plugin.app_settings_value = \
62 db_plugin.app_settings_value = \
64 'egg:rhodecode-enterprise-ce#rhodecode'
63 'egg:rhodecode-enterprise-ce#rhodecode'
65 Session().add(db_plugin)
64 Session().add(db_plugin)
66 Session().commit()
65 Session().commit()
67 for _plugin in _enabled_plugins:
66 for _plugin in _enabled_plugins:
68 db_plugin = SettingsModel().get_setting_by_name(_plugin)
67 db_plugin = SettingsModel().get_setting_by_name(_plugin)
69 if db_plugin:
68 if db_plugin:
70 Session().delete(db_plugin)
69 Session().delete(db_plugin)
71 Session().commit()
70 Session().commit()
72
71
73 response = self.app.post(url=test_url, params=params)
72 response = self.app.post(url=test_url, params=params)
74
73
75 if verify_response:
74 if verify_response:
76 assert_auth_settings_updated(response)
75 assert_auth_settings_updated(response)
77 return params
76 return params
78
77
79 def _post_ldap_settings(self, params, override=None, force=False):
78 def _post_ldap_settings(self, params, override=None, force=False):
80
79
81 params.update({
80 params.update({
82 'filter': 'user',
81 'filter': 'user',
83 'user_member_of': '',
82 'user_member_of': '',
84 'user_search_base': '',
83 'user_search_base': '',
85 'user_search_filter': 'test_filter',
84 'user_search_filter': 'test_filter',
86
85
87 'host': 'dc.example.com',
86 'host': 'dc.example.com',
88 'port': '999',
87 'port': '999',
89 'timeout': 3600,
88 'timeout': 3600,
90 'tls_kind': 'PLAIN',
89 'tls_kind': 'PLAIN',
91 'tls_reqcert': 'NEVER',
90 'tls_reqcert': 'NEVER',
92 'tls_cert_dir':'/etc/openldap/cacerts',
91 'tls_cert_dir':'/etc/openldap/cacerts',
93 'dn_user': 'test_user',
92 'dn_user': 'test_user',
94 'dn_pass': 'test_pass',
93 'dn_pass': 'test_pass',
95 'base_dn': 'test_base_dn',
94 'base_dn': 'test_base_dn',
96 'search_scope': 'BASE',
95 'search_scope': 'BASE',
97 'attr_login': 'test_attr_login',
96 'attr_login': 'test_attr_login',
98 'attr_firstname': 'ima',
97 'attr_firstname': 'ima',
99 'attr_lastname': 'tester',
98 'attr_lastname': 'tester',
100 'attr_email': 'test@example.com',
99 'attr_email': 'test@example.com',
101 'cache_ttl': '0',
100 'cache_ttl': '0',
102 })
101 })
103 if force:
102 if force:
104 params = {}
103 params = {}
105 params.update(override or {})
104 params.update(override or {})
106
105
107 test_url = '/_admin/auth/ldap/'
106 test_url = '/_admin/auth/ldap/'
108
107
109 response = self.app.post(url=test_url, params=params)
108 response = self.app.post(url=test_url, params=params)
110 return response
109 return response
111
110
112 def test_index(self):
111 def test_index(self):
113 response = self.app.get('/_admin/auth')
112 response = self.app.get('/_admin/auth')
114 response.mustcontain('Authentication Plugins')
113 response.mustcontain('Authentication Plugins')
115
114
116 @pytest.mark.parametrize("disable_plugin, needs_import", [
115 @pytest.mark.parametrize("disable_plugin, needs_import", [
117 ('egg:rhodecode-enterprise-ce#headers', None),
116 ('egg:rhodecode-enterprise-ce#headers', None),
118 ('egg:rhodecode-enterprise-ce#crowd', None),
117 ('egg:rhodecode-enterprise-ce#crowd', None),
119 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
118 ('egg:rhodecode-enterprise-ce#jasig_cas', None),
120 ('egg:rhodecode-enterprise-ce#ldap', None),
119 ('egg:rhodecode-enterprise-ce#ldap', None),
121 ('egg:rhodecode-enterprise-ce#pam', "pam"),
120 ('egg:rhodecode-enterprise-ce#pam', "pam"),
122 ])
121 ])
123 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
122 def test_disable_plugin(self, csrf_token, disable_plugin, needs_import):
124 # TODO: johbo: "pam" is currently not available on darwin,
123 # TODO: johbo: "pam" is currently not available on darwin,
125 # although the docs state that it should work on darwin.
124 # although the docs state that it should work on darwin.
126 if needs_import:
125 if needs_import:
127 pytest.importorskip(needs_import)
126 pytest.importorskip(needs_import)
128
127
129 self._enable_plugins(
128 self._enable_plugins(
130 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
129 'egg:rhodecode-enterprise-ce#rhodecode,' + disable_plugin,
131 csrf_token, verify_response=True)
130 csrf_token, verify_response=True)
132
131
133 self._enable_plugins(
132 self._enable_plugins(
134 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
133 'egg:rhodecode-enterprise-ce#rhodecode', csrf_token,
135 verify_response=True)
134 verify_response=True)
136
135
137 def test_ldap_save_settings(self, csrf_token):
136 def test_ldap_save_settings(self, csrf_token):
138 params = self._enable_plugins(
137 params = self._enable_plugins(
139 'egg:rhodecode-enterprise-ce#rhodecode,'
138 'egg:rhodecode-enterprise-ce#rhodecode,'
140 'egg:rhodecode-enterprise-ce#ldap',
139 'egg:rhodecode-enterprise-ce#ldap',
141 csrf_token)
140 csrf_token)
142 response = self._post_ldap_settings(params)
141 response = self._post_ldap_settings(params)
143 assert_auth_settings_updated(response)
142 assert_auth_settings_updated(response)
144
143
145 new_settings = SettingsModel().get_auth_settings()
144 new_settings = SettingsModel().get_auth_settings()
146 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
145 assert new_settings['auth_ldap_host'] == u'dc.example.com', \
147 'fail db write compare'
146 'fail db write compare'
148
147
149 def test_ldap_error_form_wrong_port_number(self, csrf_token):
148 def test_ldap_error_form_wrong_port_number(self, csrf_token):
150 params = self._enable_plugins(
149 params = self._enable_plugins(
151 'egg:rhodecode-enterprise-ce#rhodecode,'
150 'egg:rhodecode-enterprise-ce#rhodecode,'
152 'egg:rhodecode-enterprise-ce#ldap',
151 'egg:rhodecode-enterprise-ce#ldap',
153 csrf_token)
152 csrf_token)
154 invalid_port_value = 'invalid-port-number'
153 invalid_port_value = 'invalid-port-number'
155 response = self._post_ldap_settings(params, override={
154 response = self._post_ldap_settings(params, override={
156 'port': invalid_port_value,
155 'port': invalid_port_value,
157 })
156 })
158 assertr = response.assert_response()
157 assertr = response.assert_response()
159 assertr.element_contains(
158 assertr.element_contains(
160 '.form .field #port ~ .error-message',
159 '.form .field #port ~ .error-message',
161 invalid_port_value)
160 invalid_port_value)
162
161
163 def test_ldap_error_form(self, csrf_token):
162 def test_ldap_error_form(self, csrf_token):
164 params = self._enable_plugins(
163 params = self._enable_plugins(
165 'egg:rhodecode-enterprise-ce#rhodecode,'
164 'egg:rhodecode-enterprise-ce#rhodecode,'
166 'egg:rhodecode-enterprise-ce#ldap',
165 'egg:rhodecode-enterprise-ce#ldap',
167 csrf_token)
166 csrf_token)
168 response = self._post_ldap_settings(params, override={
167 response = self._post_ldap_settings(params, override={
169 'attr_login': '',
168 'attr_login': '',
170 })
169 })
171 response.mustcontain("""<span class="error-message">The LDAP Login"""
170 response.mustcontain("""<span class="error-message">The LDAP Login"""
172 """ attribute of the CN must be specified""")
171 """ attribute of the CN must be specified""")
173
172
174 def test_post_ldap_group_settings(self, csrf_token):
173 def test_post_ldap_group_settings(self, csrf_token):
175 params = self._enable_plugins(
174 params = self._enable_plugins(
176 'egg:rhodecode-enterprise-ce#rhodecode,'
175 'egg:rhodecode-enterprise-ce#rhodecode,'
177 'egg:rhodecode-enterprise-ce#ldap',
176 'egg:rhodecode-enterprise-ce#ldap',
178 csrf_token)
177 csrf_token)
179
178
180 response = self._post_ldap_settings(params, override={
179 response = self._post_ldap_settings(params, override={
181 'host': 'dc-legacy.example.com',
180 'host': 'dc-legacy.example.com',
182 'port': '999',
181 'port': '999',
183 'tls_kind': 'PLAIN',
182 'tls_kind': 'PLAIN',
184 'tls_reqcert': 'NEVER',
183 'tls_reqcert': 'NEVER',
185 'dn_user': 'test_user',
184 'dn_user': 'test_user',
186 'dn_pass': 'test_pass',
185 'dn_pass': 'test_pass',
187 'base_dn': 'test_base_dn',
186 'base_dn': 'test_base_dn',
188 'filter': 'test_filter',
187 'filter': 'test_filter',
189 'search_scope': 'BASE',
188 'search_scope': 'BASE',
190 'attr_login': 'test_attr_login',
189 'attr_login': 'test_attr_login',
191 'attr_firstname': 'ima',
190 'attr_firstname': 'ima',
192 'attr_lastname': 'tester',
191 'attr_lastname': 'tester',
193 'attr_email': 'test@example.com',
192 'attr_email': 'test@example.com',
194 'cache_ttl': '60',
193 'cache_ttl': '60',
195 'csrf_token': csrf_token,
194 'csrf_token': csrf_token,
196 }
195 }
197 )
196 )
198 assert_auth_settings_updated(response)
197 assert_auth_settings_updated(response)
199
198
200 new_settings = SettingsModel().get_auth_settings()
199 new_settings = SettingsModel().get_auth_settings()
201 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
200 assert new_settings['auth_ldap_host'] == u'dc-legacy.example.com', \
202 'fail db write compare'
201 'fail db write compare'
@@ -1,85 +1,84 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22
21
23 from rhodecode.tests import assert_session_flash
22 from rhodecode.tests import assert_session_flash
24 from rhodecode.model.settings import SettingsModel
23 from rhodecode.model.settings import SettingsModel
25
24
26
25
27 def route_path(name, params=None, **kwargs):
26 def route_path(name, params=None, **kwargs):
28 import urllib.request, urllib.parse, urllib.error
27 import urllib.request, urllib.parse, urllib.error
29 from rhodecode.apps._base import ADMIN_PREFIX
28 from rhodecode.apps._base import ADMIN_PREFIX
30
29
31 base_url = {
30 base_url = {
32 'admin_defaults_repositories':
31 'admin_defaults_repositories':
33 ADMIN_PREFIX + '/defaults/repositories',
32 ADMIN_PREFIX + '/defaults/repositories',
34 'admin_defaults_repositories_update':
33 'admin_defaults_repositories_update':
35 ADMIN_PREFIX + '/defaults/repositories/update',
34 ADMIN_PREFIX + '/defaults/repositories/update',
36 }[name].format(**kwargs)
35 }[name].format(**kwargs)
37
36
38 if params:
37 if params:
39 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
38 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 return base_url
39 return base_url
41
40
42
41
43 @pytest.mark.usefixtures("app")
42 @pytest.mark.usefixtures("app")
44 class TestDefaultsView(object):
43 class TestDefaultsView(object):
45
44
46 def test_index(self, autologin_user):
45 def test_index(self, autologin_user):
47 response = self.app.get(route_path('admin_defaults_repositories'))
46 response = self.app.get(route_path('admin_defaults_repositories'))
48 response.mustcontain('default_repo_private')
47 response.mustcontain('default_repo_private')
49 response.mustcontain('default_repo_enable_statistics')
48 response.mustcontain('default_repo_enable_statistics')
50 response.mustcontain('default_repo_enable_downloads')
49 response.mustcontain('default_repo_enable_downloads')
51 response.mustcontain('default_repo_enable_locking')
50 response.mustcontain('default_repo_enable_locking')
52
51
53 def test_update_params_true_hg(self, autologin_user, csrf_token):
52 def test_update_params_true_hg(self, autologin_user, csrf_token):
54 params = {
53 params = {
55 'default_repo_enable_locking': True,
54 'default_repo_enable_locking': True,
56 'default_repo_enable_downloads': True,
55 'default_repo_enable_downloads': True,
57 'default_repo_enable_statistics': True,
56 'default_repo_enable_statistics': True,
58 'default_repo_private': True,
57 'default_repo_private': True,
59 'default_repo_type': 'hg',
58 'default_repo_type': 'hg',
60 'csrf_token': csrf_token,
59 'csrf_token': csrf_token,
61 }
60 }
62 response = self.app.post(
61 response = self.app.post(
63 route_path('admin_defaults_repositories_update'), params=params)
62 route_path('admin_defaults_repositories_update'), params=params)
64 assert_session_flash(response, 'Default settings updated successfully')
63 assert_session_flash(response, 'Default settings updated successfully')
65
64
66 defs = SettingsModel().get_default_repo_settings()
65 defs = SettingsModel().get_default_repo_settings()
67 del params['csrf_token']
66 del params['csrf_token']
68 assert params == defs
67 assert params == defs
69
68
70 def test_update_params_false_git(self, autologin_user, csrf_token):
69 def test_update_params_false_git(self, autologin_user, csrf_token):
71 params = {
70 params = {
72 'default_repo_enable_locking': False,
71 'default_repo_enable_locking': False,
73 'default_repo_enable_downloads': False,
72 'default_repo_enable_downloads': False,
74 'default_repo_enable_statistics': False,
73 'default_repo_enable_statistics': False,
75 'default_repo_private': False,
74 'default_repo_private': False,
76 'default_repo_type': 'git',
75 'default_repo_type': 'git',
77 'csrf_token': csrf_token,
76 'csrf_token': csrf_token,
78 }
77 }
79 response = self.app.post(
78 response = self.app.post(
80 route_path('admin_defaults_repositories_update'), params=params)
79 route_path('admin_defaults_repositories_update'), params=params)
81 assert_session_flash(response, 'Default settings updated successfully')
80 assert_session_flash(response, 'Default settings updated successfully')
82
81
83 defs = SettingsModel().get_default_repo_settings()
82 defs = SettingsModel().get_default_repo_settings()
84 del params['csrf_token']
83 del params['csrf_token']
85 assert params == defs
84 assert params == defs
@@ -1,82 +1,81 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22
21
23 from rhodecode.tests import TestController
22 from rhodecode.tests import TestController
24 from rhodecode.tests.fixture import Fixture
23 from rhodecode.tests.fixture import Fixture
25
24
26 fixture = Fixture()
25 fixture = Fixture()
27
26
28
27
29 def route_path(name, params=None, **kwargs):
28 def route_path(name, params=None, **kwargs):
30 import urllib.request, urllib.parse, urllib.error
29 import urllib.request, urllib.parse, urllib.error
31 from rhodecode.apps._base import ADMIN_PREFIX
30 from rhodecode.apps._base import ADMIN_PREFIX
32
31
33 base_url = {
32 base_url = {
34 'admin_home': ADMIN_PREFIX,
33 'admin_home': ADMIN_PREFIX,
35 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
34 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
36 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
35 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
37 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
36 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
38 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
37 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
39
38
40 }[name].format(**kwargs)
39 }[name].format(**kwargs)
41
40
42 if params:
41 if params:
43 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
44 return base_url
43 return base_url
45
44
46
45
47 class TestAdminMainView(TestController):
46 class TestAdminMainView(TestController):
48
47
49 def test_access_admin_home(self):
48 def test_access_admin_home(self):
50 self.log_user()
49 self.log_user()
51 response = self.app.get(route_path('admin_home'), status=200)
50 response = self.app.get(route_path('admin_home'), status=200)
52 response.mustcontain("Administration area")
51 response.mustcontain("Administration area")
53
52
54 def test_redirect_pull_request_view(self, view):
53 def test_redirect_pull_request_view(self, view):
55 self.log_user()
54 self.log_user()
56 self.app.get(
55 self.app.get(
57 route_path(view, pull_request_id='xxxx'),
56 route_path(view, pull_request_id='xxxx'),
58 status=404)
57 status=404)
59
58
60 @pytest.mark.backends("git", "hg")
59 @pytest.mark.backends("git", "hg")
61 @pytest.mark.parametrize('view', [
60 @pytest.mark.parametrize('view', [
62 'pull_requests_global',
61 'pull_requests_global',
63 'pull_requests_global_0',
62 'pull_requests_global_0',
64 'pull_requests_global_1',
63 'pull_requests_global_1',
65 ])
64 ])
66 def test_redirect_pull_request_view(self, view, pr_util):
65 def test_redirect_pull_request_view(self, view, pr_util):
67 self.log_user()
66 self.log_user()
68 pull_request = pr_util.create_pull_request()
67 pull_request = pr_util.create_pull_request()
69 pull_request_id = pull_request.pull_request_id
68 pull_request_id = pull_request.pull_request_id
70 repo_name = pull_request.target_repo.repo_name
69 repo_name = pull_request.target_repo.repo_name
71
70
72 response = self.app.get(
71 response = self.app.get(
73 route_path(view, pull_request_id=pull_request_id),
72 route_path(view, pull_request_id=pull_request_id),
74 status=302)
73 status=302)
75 assert response.location.endswith(
74 assert response.location.endswith(
76 'pull-request/{}'.format(pull_request_id))
75 'pull-request/{}'.format(pull_request_id))
77
76
78 redirect_url = route_path(
77 redirect_url = route_path(
79 'pullrequest_show', repo_name=repo_name,
78 'pullrequest_show', repo_name=repo_name,
80 pull_request_id=pull_request_id)
79 pull_request_id=pull_request_id)
81
80
82 assert redirect_url in response.location
81 assert redirect_url in response.location
@@ -1,299 +1,298 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import mock
20 import mock
22 import pytest
21 import pytest
23 from rhodecode.model.db import User, UserIpMap
22 from rhodecode.model.db import User, UserIpMap
24 from rhodecode.model.meta import Session
23 from rhodecode.model.meta import Session
25 from rhodecode.model.permission import PermissionModel
24 from rhodecode.model.permission import PermissionModel
26 from rhodecode.model.ssh_key import SshKeyModel
25 from rhodecode.model.ssh_key import SshKeyModel
27 from rhodecode.tests import (
26 from rhodecode.tests import (
28 TestController, clear_cache_regions, assert_session_flash)
27 TestController, clear_cache_regions, assert_session_flash)
29
28
30
29
31 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
32 import urllib.request, urllib.parse, urllib.error
31 import urllib.request, urllib.parse, urllib.error
33 from rhodecode.apps._base import ADMIN_PREFIX
32 from rhodecode.apps._base import ADMIN_PREFIX
34
33
35 base_url = {
34 base_url = {
36 'edit_user_ips':
35 'edit_user_ips':
37 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
36 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
38 'edit_user_ips_add':
37 'edit_user_ips_add':
39 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
38 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
40 'edit_user_ips_delete':
39 'edit_user_ips_delete':
41 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
40 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
42
41
43 'admin_permissions_application':
42 'admin_permissions_application':
44 ADMIN_PREFIX + '/permissions/application',
43 ADMIN_PREFIX + '/permissions/application',
45 'admin_permissions_application_update':
44 'admin_permissions_application_update':
46 ADMIN_PREFIX + '/permissions/application/update',
45 ADMIN_PREFIX + '/permissions/application/update',
47
46
48 'admin_permissions_global':
47 'admin_permissions_global':
49 ADMIN_PREFIX + '/permissions/global',
48 ADMIN_PREFIX + '/permissions/global',
50 'admin_permissions_global_update':
49 'admin_permissions_global_update':
51 ADMIN_PREFIX + '/permissions/global/update',
50 ADMIN_PREFIX + '/permissions/global/update',
52
51
53 'admin_permissions_object':
52 'admin_permissions_object':
54 ADMIN_PREFIX + '/permissions/object',
53 ADMIN_PREFIX + '/permissions/object',
55 'admin_permissions_object_update':
54 'admin_permissions_object_update':
56 ADMIN_PREFIX + '/permissions/object/update',
55 ADMIN_PREFIX + '/permissions/object/update',
57
56
58 'admin_permissions_ips':
57 'admin_permissions_ips':
59 ADMIN_PREFIX + '/permissions/ips',
58 ADMIN_PREFIX + '/permissions/ips',
60 'admin_permissions_overview':
59 'admin_permissions_overview':
61 ADMIN_PREFIX + '/permissions/overview',
60 ADMIN_PREFIX + '/permissions/overview',
62
61
63 'admin_permissions_ssh_keys':
62 'admin_permissions_ssh_keys':
64 ADMIN_PREFIX + '/permissions/ssh_keys',
63 ADMIN_PREFIX + '/permissions/ssh_keys',
65 'admin_permissions_ssh_keys_data':
64 'admin_permissions_ssh_keys_data':
66 ADMIN_PREFIX + '/permissions/ssh_keys/data',
65 ADMIN_PREFIX + '/permissions/ssh_keys/data',
67 'admin_permissions_ssh_keys_update':
66 'admin_permissions_ssh_keys_update':
68 ADMIN_PREFIX + '/permissions/ssh_keys/update'
67 ADMIN_PREFIX + '/permissions/ssh_keys/update'
69
68
70 }[name].format(**kwargs)
69 }[name].format(**kwargs)
71
70
72 if params:
71 if params:
73 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
72 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
74 return base_url
73 return base_url
75
74
76
75
77 class TestAdminPermissionsController(TestController):
76 class TestAdminPermissionsController(TestController):
78
77
79 @pytest.fixture(scope='class', autouse=True)
78 @pytest.fixture(scope='class', autouse=True)
80 def prepare(self, request):
79 def prepare(self, request):
81 # cleanup and reset to default permissions after
80 # cleanup and reset to default permissions after
82 @request.addfinalizer
81 @request.addfinalizer
83 def cleanup():
82 def cleanup():
84 PermissionModel().create_default_user_permissions(
83 PermissionModel().create_default_user_permissions(
85 User.get_default_user(), force=True)
84 User.get_default_user(), force=True)
86
85
87 def test_index_application(self):
86 def test_index_application(self):
88 self.log_user()
87 self.log_user()
89 self.app.get(route_path('admin_permissions_application'))
88 self.app.get(route_path('admin_permissions_application'))
90
89
91 @pytest.mark.parametrize(
90 @pytest.mark.parametrize(
92 'anonymous, default_register, default_register_message, default_password_reset,'
91 'anonymous, default_register, default_register_message, default_password_reset,'
93 'default_extern_activate, expect_error, expect_form_error', [
92 'default_extern_activate, expect_error, expect_form_error', [
94 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
93 (True, 'hg.register.none', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
95 False, False),
94 False, False),
96 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
95 (True, 'hg.register.manual_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.auto',
97 False, False),
96 False, False),
98 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
97 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 False, False),
98 False, False),
100 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
99 (True, 'hg.register.auto_activate', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 False, False),
100 False, False),
102 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
101 (True, 'hg.register.XXX', '', 'hg.password_reset.enabled', 'hg.extern_activate.manual',
103 False, True),
102 False, True),
104 (True, '', '', 'hg.password_reset.enabled', '', True, False),
103 (True, '', '', 'hg.password_reset.enabled', '', True, False),
105 ])
104 ])
106 def test_update_application_permissions(
105 def test_update_application_permissions(
107 self, anonymous, default_register, default_register_message, default_password_reset,
106 self, anonymous, default_register, default_register_message, default_password_reset,
108 default_extern_activate, expect_error, expect_form_error):
107 default_extern_activate, expect_error, expect_form_error):
109
108
110 self.log_user()
109 self.log_user()
111
110
112 # TODO: anonymous access set here to False, breaks some other tests
111 # TODO: anonymous access set here to False, breaks some other tests
113 params = {
112 params = {
114 'csrf_token': self.csrf_token,
113 'csrf_token': self.csrf_token,
115 'anonymous': anonymous,
114 'anonymous': anonymous,
116 'default_register': default_register,
115 'default_register': default_register,
117 'default_register_message': default_register_message,
116 'default_register_message': default_register_message,
118 'default_password_reset': default_password_reset,
117 'default_password_reset': default_password_reset,
119 'default_extern_activate': default_extern_activate,
118 'default_extern_activate': default_extern_activate,
120 }
119 }
121 response = self.app.post(route_path('admin_permissions_application_update'),
120 response = self.app.post(route_path('admin_permissions_application_update'),
122 params=params)
121 params=params)
123 if expect_form_error:
122 if expect_form_error:
124 assert response.status_int == 200
123 assert response.status_int == 200
125 response.mustcontain('Value must be one of')
124 response.mustcontain('Value must be one of')
126 else:
125 else:
127 if expect_error:
126 if expect_error:
128 msg = 'Error occurred during update of permissions'
127 msg = 'Error occurred during update of permissions'
129 else:
128 else:
130 msg = 'Application permissions updated successfully'
129 msg = 'Application permissions updated successfully'
131 assert_session_flash(response, msg)
130 assert_session_flash(response, msg)
132
131
133 def test_index_object(self):
132 def test_index_object(self):
134 self.log_user()
133 self.log_user()
135 self.app.get(route_path('admin_permissions_object'))
134 self.app.get(route_path('admin_permissions_object'))
136
135
137 @pytest.mark.parametrize(
136 @pytest.mark.parametrize(
138 'repo, repo_group, user_group, expect_error, expect_form_error', [
137 'repo, repo_group, user_group, expect_error, expect_form_error', [
139 ('repository.none', 'group.none', 'usergroup.none', False, False),
138 ('repository.none', 'group.none', 'usergroup.none', False, False),
140 ('repository.read', 'group.read', 'usergroup.read', False, False),
139 ('repository.read', 'group.read', 'usergroup.read', False, False),
141 ('repository.write', 'group.write', 'usergroup.write',
140 ('repository.write', 'group.write', 'usergroup.write',
142 False, False),
141 False, False),
143 ('repository.admin', 'group.admin', 'usergroup.admin',
142 ('repository.admin', 'group.admin', 'usergroup.admin',
144 False, False),
143 False, False),
145 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
144 ('repository.XXX', 'group.admin', 'usergroup.admin', False, True),
146 ('', '', '', True, False),
145 ('', '', '', True, False),
147 ])
146 ])
148 def test_update_object_permissions(self, repo, repo_group, user_group,
147 def test_update_object_permissions(self, repo, repo_group, user_group,
149 expect_error, expect_form_error):
148 expect_error, expect_form_error):
150 self.log_user()
149 self.log_user()
151
150
152 params = {
151 params = {
153 'csrf_token': self.csrf_token,
152 'csrf_token': self.csrf_token,
154 'default_repo_perm': repo,
153 'default_repo_perm': repo,
155 'overwrite_default_repo': False,
154 'overwrite_default_repo': False,
156 'default_group_perm': repo_group,
155 'default_group_perm': repo_group,
157 'overwrite_default_group': False,
156 'overwrite_default_group': False,
158 'default_user_group_perm': user_group,
157 'default_user_group_perm': user_group,
159 'overwrite_default_user_group': False,
158 'overwrite_default_user_group': False,
160 }
159 }
161 response = self.app.post(route_path('admin_permissions_object_update'),
160 response = self.app.post(route_path('admin_permissions_object_update'),
162 params=params)
161 params=params)
163 if expect_form_error:
162 if expect_form_error:
164 assert response.status_int == 200
163 assert response.status_int == 200
165 response.mustcontain('Value must be one of')
164 response.mustcontain('Value must be one of')
166 else:
165 else:
167 if expect_error:
166 if expect_error:
168 msg = 'Error occurred during update of permissions'
167 msg = 'Error occurred during update of permissions'
169 else:
168 else:
170 msg = 'Object permissions updated successfully'
169 msg = 'Object permissions updated successfully'
171 assert_session_flash(response, msg)
170 assert_session_flash(response, msg)
172
171
173 def test_index_global(self):
172 def test_index_global(self):
174 self.log_user()
173 self.log_user()
175 self.app.get(route_path('admin_permissions_global'))
174 self.app.get(route_path('admin_permissions_global'))
176
175
177 @pytest.mark.parametrize(
176 @pytest.mark.parametrize(
178 'repo_create, repo_create_write, user_group_create, repo_group_create,'
177 'repo_create, repo_create_write, user_group_create, repo_group_create,'
179 'fork_create, inherit_default_permissions, expect_error,'
178 'fork_create, inherit_default_permissions, expect_error,'
180 'expect_form_error', [
179 'expect_form_error', [
181 ('hg.create.none', 'hg.create.write_on_repogroup.false',
180 ('hg.create.none', 'hg.create.write_on_repogroup.false',
182 'hg.usergroup.create.false', 'hg.repogroup.create.false',
181 'hg.usergroup.create.false', 'hg.repogroup.create.false',
183 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
182 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
184 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
183 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
185 'hg.usergroup.create.true', 'hg.repogroup.create.true',
184 'hg.usergroup.create.true', 'hg.repogroup.create.true',
186 'hg.fork.repository', 'hg.inherit_default_perms.false',
185 'hg.fork.repository', 'hg.inherit_default_perms.false',
187 False, False),
186 False, False),
188 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
187 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
189 'hg.usergroup.create.true', 'hg.repogroup.create.true',
188 'hg.usergroup.create.true', 'hg.repogroup.create.true',
190 'hg.fork.repository', 'hg.inherit_default_perms.false',
189 'hg.fork.repository', 'hg.inherit_default_perms.false',
191 False, True),
190 False, True),
192 ('', '', '', '', '', '', True, False),
191 ('', '', '', '', '', '', True, False),
193 ])
192 ])
194 def test_update_global_permissions(
193 def test_update_global_permissions(
195 self, repo_create, repo_create_write, user_group_create,
194 self, repo_create, repo_create_write, user_group_create,
196 repo_group_create, fork_create, inherit_default_permissions,
195 repo_group_create, fork_create, inherit_default_permissions,
197 expect_error, expect_form_error):
196 expect_error, expect_form_error):
198 self.log_user()
197 self.log_user()
199
198
200 params = {
199 params = {
201 'csrf_token': self.csrf_token,
200 'csrf_token': self.csrf_token,
202 'default_repo_create': repo_create,
201 'default_repo_create': repo_create,
203 'default_repo_create_on_write': repo_create_write,
202 'default_repo_create_on_write': repo_create_write,
204 'default_user_group_create': user_group_create,
203 'default_user_group_create': user_group_create,
205 'default_repo_group_create': repo_group_create,
204 'default_repo_group_create': repo_group_create,
206 'default_fork_create': fork_create,
205 'default_fork_create': fork_create,
207 'default_inherit_default_permissions': inherit_default_permissions
206 'default_inherit_default_permissions': inherit_default_permissions
208 }
207 }
209 response = self.app.post(route_path('admin_permissions_global_update'),
208 response = self.app.post(route_path('admin_permissions_global_update'),
210 params=params)
209 params=params)
211 if expect_form_error:
210 if expect_form_error:
212 assert response.status_int == 200
211 assert response.status_int == 200
213 response.mustcontain('Value must be one of')
212 response.mustcontain('Value must be one of')
214 else:
213 else:
215 if expect_error:
214 if expect_error:
216 msg = 'Error occurred during update of permissions'
215 msg = 'Error occurred during update of permissions'
217 else:
216 else:
218 msg = 'Global permissions updated successfully'
217 msg = 'Global permissions updated successfully'
219 assert_session_flash(response, msg)
218 assert_session_flash(response, msg)
220
219
221 def test_index_ips(self):
220 def test_index_ips(self):
222 self.log_user()
221 self.log_user()
223 response = self.app.get(route_path('admin_permissions_ips'))
222 response = self.app.get(route_path('admin_permissions_ips'))
224 response.mustcontain('All IP addresses are allowed')
223 response.mustcontain('All IP addresses are allowed')
225
224
226 def test_add_delete_ips(self):
225 def test_add_delete_ips(self):
227 clear_cache_regions(['sql_cache_short'])
226 clear_cache_regions(['sql_cache_short'])
228 self.log_user()
227 self.log_user()
229
228
230 # ADD
229 # ADD
231 default_user_id = User.get_default_user_id()
230 default_user_id = User.get_default_user_id()
232 self.app.post(
231 self.app.post(
233 route_path('edit_user_ips_add', user_id=default_user_id),
232 route_path('edit_user_ips_add', user_id=default_user_id),
234 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
233 params={'new_ip': '0.0.0.0/24', 'csrf_token': self.csrf_token})
235
234
236 response = self.app.get(route_path('admin_permissions_ips'))
235 response = self.app.get(route_path('admin_permissions_ips'))
237 response.mustcontain('0.0.0.0/24')
236 response.mustcontain('0.0.0.0/24')
238 response.mustcontain('0.0.0.0 - 0.0.0.255')
237 response.mustcontain('0.0.0.0 - 0.0.0.255')
239
238
240 # DELETE
239 # DELETE
241 default_user_id = User.get_default_user_id()
240 default_user_id = User.get_default_user_id()
242 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
241 del_ip_id = UserIpMap.query().filter(UserIpMap.user_id ==
243 default_user_id).first().ip_id
242 default_user_id).first().ip_id
244
243
245 response = self.app.post(
244 response = self.app.post(
246 route_path('edit_user_ips_delete', user_id=default_user_id),
245 route_path('edit_user_ips_delete', user_id=default_user_id),
247 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
246 params={'del_ip_id': del_ip_id, 'csrf_token': self.csrf_token})
248
247
249 assert_session_flash(response, 'Removed ip address from user whitelist')
248 assert_session_flash(response, 'Removed ip address from user whitelist')
250
249
251 clear_cache_regions(['sql_cache_short'])
250 clear_cache_regions(['sql_cache_short'])
252 response = self.app.get(route_path('admin_permissions_ips'))
251 response = self.app.get(route_path('admin_permissions_ips'))
253 response.mustcontain('All IP addresses are allowed')
252 response.mustcontain('All IP addresses are allowed')
254 response.mustcontain(no=['0.0.0.0/24'])
253 response.mustcontain(no=['0.0.0.0/24'])
255 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
254 response.mustcontain(no=['0.0.0.0 - 0.0.0.255'])
256
255
257 def test_index_overview(self):
256 def test_index_overview(self):
258 self.log_user()
257 self.log_user()
259 self.app.get(route_path('admin_permissions_overview'))
258 self.app.get(route_path('admin_permissions_overview'))
260
259
261 def test_ssh_keys(self):
260 def test_ssh_keys(self):
262 self.log_user()
261 self.log_user()
263 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
262 self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
264
263
265 def test_ssh_keys_data(self, user_util, xhr_header):
264 def test_ssh_keys_data(self, user_util, xhr_header):
266 self.log_user()
265 self.log_user()
267 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
266 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
268 extra_environ=xhr_header)
267 extra_environ=xhr_header)
269 assert response.json == {u'data': [], u'draw': None,
268 assert response.json == {u'data': [], u'draw': None,
270 u'recordsFiltered': 0, u'recordsTotal': 0}
269 u'recordsFiltered': 0, u'recordsTotal': 0}
271
270
272 dummy_user = user_util.create_user()
271 dummy_user = user_util.create_user()
273 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
272 SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
274 Session().commit()
273 Session().commit()
275 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
274 response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
276 extra_environ=xhr_header)
275 extra_environ=xhr_header)
277 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
276 assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
278
277
279 def test_ssh_keys_update(self):
278 def test_ssh_keys_update(self):
280 self.log_user()
279 self.log_user()
281 response = self.app.post(
280 response = self.app.post(
282 route_path('admin_permissions_ssh_keys_update'),
281 route_path('admin_permissions_ssh_keys_update'),
283 dict(csrf_token=self.csrf_token), status=302)
282 dict(csrf_token=self.csrf_token), status=302)
284
283
285 assert_session_flash(
284 assert_session_flash(
286 response, 'Updated SSH keys file')
285 response, 'Updated SSH keys file')
287
286
288 def test_ssh_keys_update_disabled(self):
287 def test_ssh_keys_update_disabled(self):
289 self.log_user()
288 self.log_user()
290
289
291 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
290 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
292 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
291 with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
293 return_value=False):
292 return_value=False):
294 response = self.app.post(
293 response = self.app.post(
295 route_path('admin_permissions_ssh_keys_update'),
294 route_path('admin_permissions_ssh_keys_update'),
296 dict(csrf_token=self.csrf_token), status=302)
295 dict(csrf_token=self.csrf_token), status=302)
297
296
298 assert_session_flash(
297 assert_session_flash(
299 response, 'SSH key support is disabled in .ini file') No newline at end of file
298 response, 'SSH key support is disabled in .ini file')
@@ -1,512 +1,511 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import urllib.request, urllib.parse, urllib.error
20 import urllib.request, urllib.parse, urllib.error
22
21
23 import mock
22 import mock
24 import pytest
23 import pytest
25
24
26 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.apps._base import ADMIN_PREFIX
27 from rhodecode.lib import auth
26 from rhodecode.lib import auth
28 from rhodecode.lib.utils2 import safe_str
27 from rhodecode.lib.utils2 import safe_str
29 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
30 from rhodecode.model.db import (
29 from rhodecode.model.db import (
31 Repository, RepoGroup, UserRepoToPerm, User, Permission)
30 Repository, RepoGroup, UserRepoToPerm, User, Permission)
32 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
33 from rhodecode.model.repo import RepoModel
32 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.repo_group import RepoGroupModel
33 from rhodecode.model.repo_group import RepoGroupModel
35 from rhodecode.model.user import UserModel
34 from rhodecode.model.user import UserModel
36 from rhodecode.tests import (
35 from rhodecode.tests import (
37 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
36 login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
38 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
37 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
39 from rhodecode.tests.fixture import Fixture, error_function
38 from rhodecode.tests.fixture import Fixture, error_function
40 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
39 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
41
40
42 fixture = Fixture()
41 fixture = Fixture()
43
42
44
43
45 def route_path(name, params=None, **kwargs):
44 def route_path(name, params=None, **kwargs):
46 import urllib.request, urllib.parse, urllib.error
45 import urllib.request, urllib.parse, urllib.error
47
46
48 base_url = {
47 base_url = {
49 'repos': ADMIN_PREFIX + '/repos',
48 'repos': ADMIN_PREFIX + '/repos',
50 'repos_data': ADMIN_PREFIX + '/repos_data',
49 'repos_data': ADMIN_PREFIX + '/repos_data',
51 'repo_new': ADMIN_PREFIX + '/repos/new',
50 'repo_new': ADMIN_PREFIX + '/repos/new',
52 'repo_create': ADMIN_PREFIX + '/repos/create',
51 'repo_create': ADMIN_PREFIX + '/repos/create',
53
52
54 'repo_creating_check': '/{repo_name}/repo_creating_check',
53 'repo_creating_check': '/{repo_name}/repo_creating_check',
55 }[name].format(**kwargs)
54 }[name].format(**kwargs)
56
55
57 if params:
56 if params:
58 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
57 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
59 return base_url
58 return base_url
60
59
61
60
62 def _get_permission_for_user(user, repo):
61 def _get_permission_for_user(user, repo):
63 perm = UserRepoToPerm.query()\
62 perm = UserRepoToPerm.query()\
64 .filter(UserRepoToPerm.repository ==
63 .filter(UserRepoToPerm.repository ==
65 Repository.get_by_repo_name(repo))\
64 Repository.get_by_repo_name(repo))\
66 .filter(UserRepoToPerm.user == User.get_by_username(user))\
65 .filter(UserRepoToPerm.user == User.get_by_username(user))\
67 .all()
66 .all()
68 return perm
67 return perm
69
68
70
69
71 @pytest.mark.usefixtures("app")
70 @pytest.mark.usefixtures("app")
72 class TestAdminRepos(object):
71 class TestAdminRepos(object):
73
72
74 def test_repo_list(self, autologin_user, user_util, xhr_header):
73 def test_repo_list(self, autologin_user, user_util, xhr_header):
75 repo = user_util.create_repo()
74 repo = user_util.create_repo()
76 repo_name = repo.repo_name
75 repo_name = repo.repo_name
77 response = self.app.get(
76 response = self.app.get(
78 route_path('repos_data'), status=200,
77 route_path('repos_data'), status=200,
79 extra_environ=xhr_header)
78 extra_environ=xhr_header)
80
79
81 response.mustcontain(repo_name)
80 response.mustcontain(repo_name)
82
81
83 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
82 def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
84 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
83 with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
85 response = self.app.get(route_path('repo_new'), status=200)
84 response = self.app.get(route_path('repo_new'), status=200)
86 assert_response = response.assert_response()
85 assert_response = response.assert_response()
87 element = assert_response.get_element('[name=repo_type]')
86 element = assert_response.get_element('[name=repo_type]')
88 assert element.get('value') == 'git'
87 assert element.get('value') == 'git'
89
88
90 def test_create_page_non_restricted_backends(self, autologin_user, backend):
89 def test_create_page_non_restricted_backends(self, autologin_user, backend):
91 response = self.app.get(route_path('repo_new'), status=200)
90 response = self.app.get(route_path('repo_new'), status=200)
92 assert_response = response.assert_response()
91 assert_response = response.assert_response()
93 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
92 assert ['hg', 'git', 'svn'] == [x.get('value') for x in assert_response.get_elements('[name=repo_type]')]
94
93
95 @pytest.mark.parametrize(
94 @pytest.mark.parametrize(
96 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
95 "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
97 def test_create(self, autologin_user, backend, suffix, csrf_token):
96 def test_create(self, autologin_user, backend, suffix, csrf_token):
98 repo_name_unicode = backend.new_repo_name(suffix=suffix)
97 repo_name_unicode = backend.new_repo_name(suffix=suffix)
99 repo_name = repo_name_unicode.encode('utf8')
98 repo_name = repo_name_unicode.encode('utf8')
100 description_unicode = u'description for newly created repo' + suffix
99 description_unicode = u'description for newly created repo' + suffix
101 description = description_unicode.encode('utf8')
100 description = description_unicode.encode('utf8')
102 response = self.app.post(
101 response = self.app.post(
103 route_path('repo_create'),
102 route_path('repo_create'),
104 fixture._get_repo_create_params(
103 fixture._get_repo_create_params(
105 repo_private=False,
104 repo_private=False,
106 repo_name=repo_name,
105 repo_name=repo_name,
107 repo_type=backend.alias,
106 repo_type=backend.alias,
108 repo_description=description,
107 repo_description=description,
109 csrf_token=csrf_token),
108 csrf_token=csrf_token),
110 status=302)
109 status=302)
111
110
112 self.assert_repository_is_created_correctly(
111 self.assert_repository_is_created_correctly(
113 repo_name, description, backend)
112 repo_name, description, backend)
114
113
115 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
114 def test_create_numeric_name(self, autologin_user, backend, csrf_token):
116 numeric_repo = '1234'
115 numeric_repo = '1234'
117 repo_name = numeric_repo
116 repo_name = numeric_repo
118 description = 'description for newly created repo' + numeric_repo
117 description = 'description for newly created repo' + numeric_repo
119 self.app.post(
118 self.app.post(
120 route_path('repo_create'),
119 route_path('repo_create'),
121 fixture._get_repo_create_params(
120 fixture._get_repo_create_params(
122 repo_private=False,
121 repo_private=False,
123 repo_name=repo_name,
122 repo_name=repo_name,
124 repo_type=backend.alias,
123 repo_type=backend.alias,
125 repo_description=description,
124 repo_description=description,
126 csrf_token=csrf_token))
125 csrf_token=csrf_token))
127
126
128 self.assert_repository_is_created_correctly(
127 self.assert_repository_is_created_correctly(
129 repo_name, description, backend)
128 repo_name, description, backend)
130
129
131 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
130 @pytest.mark.parametrize("suffix", [u'', u'ąćę'], ids=['', 'non-ascii'])
132 def test_create_in_group(
131 def test_create_in_group(
133 self, autologin_user, backend, suffix, csrf_token):
132 self, autologin_user, backend, suffix, csrf_token):
134 # create GROUP
133 # create GROUP
135 group_name = 'sometest_%s' % backend.alias
134 group_name = 'sometest_%s' % backend.alias
136 gr = RepoGroupModel().create(group_name=group_name,
135 gr = RepoGroupModel().create(group_name=group_name,
137 group_description='test',
136 group_description='test',
138 owner=TEST_USER_ADMIN_LOGIN)
137 owner=TEST_USER_ADMIN_LOGIN)
139 Session().commit()
138 Session().commit()
140
139
141 repo_name = u'ingroup' + suffix
140 repo_name = u'ingroup' + suffix
142 repo_name_full = RepoGroup.url_sep().join(
141 repo_name_full = RepoGroup.url_sep().join(
143 [group_name, repo_name])
142 [group_name, repo_name])
144 description = u'description for newly created repo'
143 description = u'description for newly created repo'
145 self.app.post(
144 self.app.post(
146 route_path('repo_create'),
145 route_path('repo_create'),
147 fixture._get_repo_create_params(
146 fixture._get_repo_create_params(
148 repo_private=False,
147 repo_private=False,
149 repo_name=safe_str(repo_name),
148 repo_name=safe_str(repo_name),
150 repo_type=backend.alias,
149 repo_type=backend.alias,
151 repo_description=description,
150 repo_description=description,
152 repo_group=gr.group_id,
151 repo_group=gr.group_id,
153 csrf_token=csrf_token))
152 csrf_token=csrf_token))
154
153
155 # TODO: johbo: Cleanup work to fixture
154 # TODO: johbo: Cleanup work to fixture
156 try:
155 try:
157 self.assert_repository_is_created_correctly(
156 self.assert_repository_is_created_correctly(
158 repo_name_full, description, backend)
157 repo_name_full, description, backend)
159
158
160 new_repo = RepoModel().get_by_repo_name(repo_name_full)
159 new_repo = RepoModel().get_by_repo_name(repo_name_full)
161 inherited_perms = UserRepoToPerm.query().filter(
160 inherited_perms = UserRepoToPerm.query().filter(
162 UserRepoToPerm.repository_id == new_repo.repo_id).all()
161 UserRepoToPerm.repository_id == new_repo.repo_id).all()
163 assert len(inherited_perms) == 1
162 assert len(inherited_perms) == 1
164 finally:
163 finally:
165 RepoModel().delete(repo_name_full)
164 RepoModel().delete(repo_name_full)
166 RepoGroupModel().delete(group_name)
165 RepoGroupModel().delete(group_name)
167 Session().commit()
166 Session().commit()
168
167
169 def test_create_in_group_numeric_name(
168 def test_create_in_group_numeric_name(
170 self, autologin_user, backend, csrf_token):
169 self, autologin_user, backend, csrf_token):
171 # create GROUP
170 # create GROUP
172 group_name = 'sometest_%s' % backend.alias
171 group_name = 'sometest_%s' % backend.alias
173 gr = RepoGroupModel().create(group_name=group_name,
172 gr = RepoGroupModel().create(group_name=group_name,
174 group_description='test',
173 group_description='test',
175 owner=TEST_USER_ADMIN_LOGIN)
174 owner=TEST_USER_ADMIN_LOGIN)
176 Session().commit()
175 Session().commit()
177
176
178 repo_name = '12345'
177 repo_name = '12345'
179 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
178 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
180 description = 'description for newly created repo'
179 description = 'description for newly created repo'
181 self.app.post(
180 self.app.post(
182 route_path('repo_create'),
181 route_path('repo_create'),
183 fixture._get_repo_create_params(
182 fixture._get_repo_create_params(
184 repo_private=False,
183 repo_private=False,
185 repo_name=repo_name,
184 repo_name=repo_name,
186 repo_type=backend.alias,
185 repo_type=backend.alias,
187 repo_description=description,
186 repo_description=description,
188 repo_group=gr.group_id,
187 repo_group=gr.group_id,
189 csrf_token=csrf_token))
188 csrf_token=csrf_token))
190
189
191 # TODO: johbo: Cleanup work to fixture
190 # TODO: johbo: Cleanup work to fixture
192 try:
191 try:
193 self.assert_repository_is_created_correctly(
192 self.assert_repository_is_created_correctly(
194 repo_name_full, description, backend)
193 repo_name_full, description, backend)
195
194
196 new_repo = RepoModel().get_by_repo_name(repo_name_full)
195 new_repo = RepoModel().get_by_repo_name(repo_name_full)
197 inherited_perms = UserRepoToPerm.query()\
196 inherited_perms = UserRepoToPerm.query()\
198 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
197 .filter(UserRepoToPerm.repository_id == new_repo.repo_id).all()
199 assert len(inherited_perms) == 1
198 assert len(inherited_perms) == 1
200 finally:
199 finally:
201 RepoModel().delete(repo_name_full)
200 RepoModel().delete(repo_name_full)
202 RepoGroupModel().delete(group_name)
201 RepoGroupModel().delete(group_name)
203 Session().commit()
202 Session().commit()
204
203
205 def test_create_in_group_without_needed_permissions(self, backend):
204 def test_create_in_group_without_needed_permissions(self, backend):
206 session = login_user_session(
205 session = login_user_session(
207 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
206 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
208 csrf_token = auth.get_csrf_token(session)
207 csrf_token = auth.get_csrf_token(session)
209 # revoke
208 # revoke
210 user_model = UserModel()
209 user_model = UserModel()
211 # disable fork and create on default user
210 # disable fork and create on default user
212 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
211 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
213 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
212 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
214 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
213 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
215 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
214 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
216
215
217 # disable on regular user
216 # disable on regular user
218 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
217 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
219 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
218 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
220 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
219 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
221 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
220 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
222 Session().commit()
221 Session().commit()
223
222
224 # create GROUP
223 # create GROUP
225 group_name = 'reg_sometest_%s' % backend.alias
224 group_name = 'reg_sometest_%s' % backend.alias
226 gr = RepoGroupModel().create(group_name=group_name,
225 gr = RepoGroupModel().create(group_name=group_name,
227 group_description='test',
226 group_description='test',
228 owner=TEST_USER_ADMIN_LOGIN)
227 owner=TEST_USER_ADMIN_LOGIN)
229 Session().commit()
228 Session().commit()
230 repo_group_id = gr.group_id
229 repo_group_id = gr.group_id
231
230
232 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
231 group_name_allowed = 'reg_sometest_allowed_%s' % backend.alias
233 gr_allowed = RepoGroupModel().create(
232 gr_allowed = RepoGroupModel().create(
234 group_name=group_name_allowed,
233 group_name=group_name_allowed,
235 group_description='test',
234 group_description='test',
236 owner=TEST_USER_REGULAR_LOGIN)
235 owner=TEST_USER_REGULAR_LOGIN)
237 allowed_repo_group_id = gr_allowed.group_id
236 allowed_repo_group_id = gr_allowed.group_id
238 Session().commit()
237 Session().commit()
239
238
240 repo_name = 'ingroup'
239 repo_name = 'ingroup'
241 description = 'description for newly created repo'
240 description = 'description for newly created repo'
242 response = self.app.post(
241 response = self.app.post(
243 route_path('repo_create'),
242 route_path('repo_create'),
244 fixture._get_repo_create_params(
243 fixture._get_repo_create_params(
245 repo_private=False,
244 repo_private=False,
246 repo_name=repo_name,
245 repo_name=repo_name,
247 repo_type=backend.alias,
246 repo_type=backend.alias,
248 repo_description=description,
247 repo_description=description,
249 repo_group=repo_group_id,
248 repo_group=repo_group_id,
250 csrf_token=csrf_token))
249 csrf_token=csrf_token))
251
250
252 response.mustcontain('Invalid value')
251 response.mustcontain('Invalid value')
253
252
254 # user is allowed to create in this group
253 # user is allowed to create in this group
255 repo_name = 'ingroup'
254 repo_name = 'ingroup'
256 repo_name_full = RepoGroup.url_sep().join(
255 repo_name_full = RepoGroup.url_sep().join(
257 [group_name_allowed, repo_name])
256 [group_name_allowed, repo_name])
258 description = 'description for newly created repo'
257 description = 'description for newly created repo'
259 response = self.app.post(
258 response = self.app.post(
260 route_path('repo_create'),
259 route_path('repo_create'),
261 fixture._get_repo_create_params(
260 fixture._get_repo_create_params(
262 repo_private=False,
261 repo_private=False,
263 repo_name=repo_name,
262 repo_name=repo_name,
264 repo_type=backend.alias,
263 repo_type=backend.alias,
265 repo_description=description,
264 repo_description=description,
266 repo_group=allowed_repo_group_id,
265 repo_group=allowed_repo_group_id,
267 csrf_token=csrf_token))
266 csrf_token=csrf_token))
268
267
269 # TODO: johbo: Cleanup in pytest fixture
268 # TODO: johbo: Cleanup in pytest fixture
270 try:
269 try:
271 self.assert_repository_is_created_correctly(
270 self.assert_repository_is_created_correctly(
272 repo_name_full, description, backend)
271 repo_name_full, description, backend)
273
272
274 new_repo = RepoModel().get_by_repo_name(repo_name_full)
273 new_repo = RepoModel().get_by_repo_name(repo_name_full)
275 inherited_perms = UserRepoToPerm.query().filter(
274 inherited_perms = UserRepoToPerm.query().filter(
276 UserRepoToPerm.repository_id == new_repo.repo_id).all()
275 UserRepoToPerm.repository_id == new_repo.repo_id).all()
277 assert len(inherited_perms) == 1
276 assert len(inherited_perms) == 1
278
277
279 assert repo_on_filesystem(repo_name_full)
278 assert repo_on_filesystem(repo_name_full)
280 finally:
279 finally:
281 RepoModel().delete(repo_name_full)
280 RepoModel().delete(repo_name_full)
282 RepoGroupModel().delete(group_name)
281 RepoGroupModel().delete(group_name)
283 RepoGroupModel().delete(group_name_allowed)
282 RepoGroupModel().delete(group_name_allowed)
284 Session().commit()
283 Session().commit()
285
284
286 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
285 def test_create_in_group_inherit_permissions(self, autologin_user, backend,
287 csrf_token):
286 csrf_token):
288 # create GROUP
287 # create GROUP
289 group_name = 'sometest_%s' % backend.alias
288 group_name = 'sometest_%s' % backend.alias
290 gr = RepoGroupModel().create(group_name=group_name,
289 gr = RepoGroupModel().create(group_name=group_name,
291 group_description='test',
290 group_description='test',
292 owner=TEST_USER_ADMIN_LOGIN)
291 owner=TEST_USER_ADMIN_LOGIN)
293 perm = Permission.get_by_key('repository.write')
292 perm = Permission.get_by_key('repository.write')
294 RepoGroupModel().grant_user_permission(
293 RepoGroupModel().grant_user_permission(
295 gr, TEST_USER_REGULAR_LOGIN, perm)
294 gr, TEST_USER_REGULAR_LOGIN, perm)
296
295
297 # add repo permissions
296 # add repo permissions
298 Session().commit()
297 Session().commit()
299 repo_group_id = gr.group_id
298 repo_group_id = gr.group_id
300 repo_name = 'ingroup_inherited_%s' % backend.alias
299 repo_name = 'ingroup_inherited_%s' % backend.alias
301 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
300 repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
302 description = 'description for newly created repo'
301 description = 'description for newly created repo'
303 self.app.post(
302 self.app.post(
304 route_path('repo_create'),
303 route_path('repo_create'),
305 fixture._get_repo_create_params(
304 fixture._get_repo_create_params(
306 repo_private=False,
305 repo_private=False,
307 repo_name=repo_name,
306 repo_name=repo_name,
308 repo_type=backend.alias,
307 repo_type=backend.alias,
309 repo_description=description,
308 repo_description=description,
310 repo_group=repo_group_id,
309 repo_group=repo_group_id,
311 repo_copy_permissions=True,
310 repo_copy_permissions=True,
312 csrf_token=csrf_token))
311 csrf_token=csrf_token))
313
312
314 # TODO: johbo: Cleanup to pytest fixture
313 # TODO: johbo: Cleanup to pytest fixture
315 try:
314 try:
316 self.assert_repository_is_created_correctly(
315 self.assert_repository_is_created_correctly(
317 repo_name_full, description, backend)
316 repo_name_full, description, backend)
318 except Exception:
317 except Exception:
319 RepoGroupModel().delete(group_name)
318 RepoGroupModel().delete(group_name)
320 Session().commit()
319 Session().commit()
321 raise
320 raise
322
321
323 # check if inherited permissions are applied
322 # check if inherited permissions are applied
324 new_repo = RepoModel().get_by_repo_name(repo_name_full)
323 new_repo = RepoModel().get_by_repo_name(repo_name_full)
325 inherited_perms = UserRepoToPerm.query().filter(
324 inherited_perms = UserRepoToPerm.query().filter(
326 UserRepoToPerm.repository_id == new_repo.repo_id).all()
325 UserRepoToPerm.repository_id == new_repo.repo_id).all()
327 assert len(inherited_perms) == 2
326 assert len(inherited_perms) == 2
328
327
329 assert TEST_USER_REGULAR_LOGIN in [
328 assert TEST_USER_REGULAR_LOGIN in [
330 x.user.username for x in inherited_perms]
329 x.user.username for x in inherited_perms]
331 assert 'repository.write' in [
330 assert 'repository.write' in [
332 x.permission.permission_name for x in inherited_perms]
331 x.permission.permission_name for x in inherited_perms]
333
332
334 RepoModel().delete(repo_name_full)
333 RepoModel().delete(repo_name_full)
335 RepoGroupModel().delete(group_name)
334 RepoGroupModel().delete(group_name)
336 Session().commit()
335 Session().commit()
337
336
338 @pytest.mark.xfail_backends(
337 @pytest.mark.xfail_backends(
339 "git", "hg", reason="Missing reposerver support")
338 "git", "hg", reason="Missing reposerver support")
340 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
339 def test_create_with_clone_uri(self, autologin_user, backend, reposerver,
341 csrf_token):
340 csrf_token):
342 source_repo = backend.create_repo(number_of_commits=2)
341 source_repo = backend.create_repo(number_of_commits=2)
343 source_repo_name = source_repo.repo_name
342 source_repo_name = source_repo.repo_name
344 reposerver.serve(source_repo.scm_instance())
343 reposerver.serve(source_repo.scm_instance())
345
344
346 repo_name = backend.new_repo_name()
345 repo_name = backend.new_repo_name()
347 response = self.app.post(
346 response = self.app.post(
348 route_path('repo_create'),
347 route_path('repo_create'),
349 fixture._get_repo_create_params(
348 fixture._get_repo_create_params(
350 repo_private=False,
349 repo_private=False,
351 repo_name=repo_name,
350 repo_name=repo_name,
352 repo_type=backend.alias,
351 repo_type=backend.alias,
353 repo_description='',
352 repo_description='',
354 clone_uri=reposerver.url,
353 clone_uri=reposerver.url,
355 csrf_token=csrf_token),
354 csrf_token=csrf_token),
356 status=302)
355 status=302)
357
356
358 # Should be redirected to the creating page
357 # Should be redirected to the creating page
359 response.mustcontain('repo_creating')
358 response.mustcontain('repo_creating')
360
359
361 # Expecting that both repositories have same history
360 # Expecting that both repositories have same history
362 source_repo = RepoModel().get_by_repo_name(source_repo_name)
361 source_repo = RepoModel().get_by_repo_name(source_repo_name)
363 source_vcs = source_repo.scm_instance()
362 source_vcs = source_repo.scm_instance()
364 repo = RepoModel().get_by_repo_name(repo_name)
363 repo = RepoModel().get_by_repo_name(repo_name)
365 repo_vcs = repo.scm_instance()
364 repo_vcs = repo.scm_instance()
366 assert source_vcs[0].message == repo_vcs[0].message
365 assert source_vcs[0].message == repo_vcs[0].message
367 assert source_vcs.count() == repo_vcs.count()
366 assert source_vcs.count() == repo_vcs.count()
368 assert source_vcs.commit_ids == repo_vcs.commit_ids
367 assert source_vcs.commit_ids == repo_vcs.commit_ids
369
368
370 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
369 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
371 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
370 def test_create_remote_repo_wrong_clone_uri(self, autologin_user, backend,
372 csrf_token):
371 csrf_token):
373 repo_name = backend.new_repo_name()
372 repo_name = backend.new_repo_name()
374 description = 'description for newly created repo'
373 description = 'description for newly created repo'
375 response = self.app.post(
374 response = self.app.post(
376 route_path('repo_create'),
375 route_path('repo_create'),
377 fixture._get_repo_create_params(
376 fixture._get_repo_create_params(
378 repo_private=False,
377 repo_private=False,
379 repo_name=repo_name,
378 repo_name=repo_name,
380 repo_type=backend.alias,
379 repo_type=backend.alias,
381 repo_description=description,
380 repo_description=description,
382 clone_uri='http://repo.invalid/repo',
381 clone_uri='http://repo.invalid/repo',
383 csrf_token=csrf_token))
382 csrf_token=csrf_token))
384 response.mustcontain('invalid clone url')
383 response.mustcontain('invalid clone url')
385
384
386 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
385 @pytest.mark.xfail_backends("svn", reason="Depends on import support")
387 def test_create_remote_repo_wrong_clone_uri_hg_svn(
386 def test_create_remote_repo_wrong_clone_uri_hg_svn(
388 self, autologin_user, backend, csrf_token):
387 self, autologin_user, backend, csrf_token):
389 repo_name = backend.new_repo_name()
388 repo_name = backend.new_repo_name()
390 description = 'description for newly created repo'
389 description = 'description for newly created repo'
391 response = self.app.post(
390 response = self.app.post(
392 route_path('repo_create'),
391 route_path('repo_create'),
393 fixture._get_repo_create_params(
392 fixture._get_repo_create_params(
394 repo_private=False,
393 repo_private=False,
395 repo_name=repo_name,
394 repo_name=repo_name,
396 repo_type=backend.alias,
395 repo_type=backend.alias,
397 repo_description=description,
396 repo_description=description,
398 clone_uri='svn+http://svn.invalid/repo',
397 clone_uri='svn+http://svn.invalid/repo',
399 csrf_token=csrf_token))
398 csrf_token=csrf_token))
400 response.mustcontain('invalid clone url')
399 response.mustcontain('invalid clone url')
401
400
402 def test_create_with_git_suffix(
401 def test_create_with_git_suffix(
403 self, autologin_user, backend, csrf_token):
402 self, autologin_user, backend, csrf_token):
404 repo_name = backend.new_repo_name() + ".git"
403 repo_name = backend.new_repo_name() + ".git"
405 description = 'description for newly created repo'
404 description = 'description for newly created repo'
406 response = self.app.post(
405 response = self.app.post(
407 route_path('repo_create'),
406 route_path('repo_create'),
408 fixture._get_repo_create_params(
407 fixture._get_repo_create_params(
409 repo_private=False,
408 repo_private=False,
410 repo_name=repo_name,
409 repo_name=repo_name,
411 repo_type=backend.alias,
410 repo_type=backend.alias,
412 repo_description=description,
411 repo_description=description,
413 csrf_token=csrf_token))
412 csrf_token=csrf_token))
414 response.mustcontain('Repository name cannot end with .git')
413 response.mustcontain('Repository name cannot end with .git')
415
414
416 def test_default_user_cannot_access_private_repo_in_a_group(
415 def test_default_user_cannot_access_private_repo_in_a_group(
417 self, autologin_user, user_util, backend):
416 self, autologin_user, user_util, backend):
418
417
419 group = user_util.create_repo_group()
418 group = user_util.create_repo_group()
420
419
421 repo = backend.create_repo(
420 repo = backend.create_repo(
422 repo_private=True, repo_group=group, repo_copy_permissions=True)
421 repo_private=True, repo_group=group, repo_copy_permissions=True)
423
422
424 permissions = _get_permission_for_user(
423 permissions = _get_permission_for_user(
425 user='default', repo=repo.repo_name)
424 user='default', repo=repo.repo_name)
426 assert len(permissions) == 1
425 assert len(permissions) == 1
427 assert permissions[0].permission.permission_name == 'repository.none'
426 assert permissions[0].permission.permission_name == 'repository.none'
428 assert permissions[0].repository.private is True
427 assert permissions[0].repository.private is True
429
428
430 def test_create_on_top_level_without_permissions(self, backend):
429 def test_create_on_top_level_without_permissions(self, backend):
431 session = login_user_session(
430 session = login_user_session(
432 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
431 self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
433 csrf_token = auth.get_csrf_token(session)
432 csrf_token = auth.get_csrf_token(session)
434
433
435 # revoke
434 # revoke
436 user_model = UserModel()
435 user_model = UserModel()
437 # disable fork and create on default user
436 # disable fork and create on default user
438 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
437 user_model.revoke_perm(User.DEFAULT_USER, 'hg.create.repository')
439 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
438 user_model.grant_perm(User.DEFAULT_USER, 'hg.create.none')
440 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
439 user_model.revoke_perm(User.DEFAULT_USER, 'hg.fork.repository')
441 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
440 user_model.grant_perm(User.DEFAULT_USER, 'hg.fork.none')
442
441
443 # disable on regular user
442 # disable on regular user
444 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
443 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.repository')
445 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
444 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.create.none')
446 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
445 user_model.revoke_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.repository')
447 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
446 user_model.grant_perm(TEST_USER_REGULAR_LOGIN, 'hg.fork.none')
448 Session().commit()
447 Session().commit()
449
448
450 repo_name = backend.new_repo_name()
449 repo_name = backend.new_repo_name()
451 description = 'description for newly created repo'
450 description = 'description for newly created repo'
452 response = self.app.post(
451 response = self.app.post(
453 route_path('repo_create'),
452 route_path('repo_create'),
454 fixture._get_repo_create_params(
453 fixture._get_repo_create_params(
455 repo_private=False,
454 repo_private=False,
456 repo_name=repo_name,
455 repo_name=repo_name,
457 repo_type=backend.alias,
456 repo_type=backend.alias,
458 repo_description=description,
457 repo_description=description,
459 csrf_token=csrf_token))
458 csrf_token=csrf_token))
460
459
461 response.mustcontain(
460 response.mustcontain(
462 u"You do not have the permission to store repositories in "
461 u"You do not have the permission to store repositories in "
463 u"the root location.")
462 u"the root location.")
464
463
465 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
464 @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
466 def test_create_repo_when_filesystem_op_fails(
465 def test_create_repo_when_filesystem_op_fails(
467 self, autologin_user, backend, csrf_token):
466 self, autologin_user, backend, csrf_token):
468 repo_name = backend.new_repo_name()
467 repo_name = backend.new_repo_name()
469 description = 'description for newly created repo'
468 description = 'description for newly created repo'
470
469
471 response = self.app.post(
470 response = self.app.post(
472 route_path('repo_create'),
471 route_path('repo_create'),
473 fixture._get_repo_create_params(
472 fixture._get_repo_create_params(
474 repo_private=False,
473 repo_private=False,
475 repo_name=repo_name,
474 repo_name=repo_name,
476 repo_type=backend.alias,
475 repo_type=backend.alias,
477 repo_description=description,
476 repo_description=description,
478 csrf_token=csrf_token))
477 csrf_token=csrf_token))
479
478
480 assert_session_flash(
479 assert_session_flash(
481 response, 'Error creating repository %s' % repo_name)
480 response, 'Error creating repository %s' % repo_name)
482 # repo must not be in db
481 # repo must not be in db
483 assert backend.repo is None
482 assert backend.repo is None
484 # repo must not be in filesystem !
483 # repo must not be in filesystem !
485 assert not repo_on_filesystem(repo_name)
484 assert not repo_on_filesystem(repo_name)
486
485
487 def assert_repository_is_created_correctly(
486 def assert_repository_is_created_correctly(
488 self, repo_name, description, backend):
487 self, repo_name, description, backend):
489 repo_name_utf8 = safe_str(repo_name)
488 repo_name_utf8 = safe_str(repo_name)
490
489
491 # run the check page that triggers the flash message
490 # run the check page that triggers the flash message
492 response = self.app.get(
491 response = self.app.get(
493 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
492 route_path('repo_creating_check', repo_name=safe_str(repo_name)))
494 assert response.json == {u'result': True}
493 assert response.json == {u'result': True}
495
494
496 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
495 flash_msg = u'Created repository <a href="/{}">{}</a>'.format(
497 urllib.parse.quote(repo_name_utf8), repo_name)
496 urllib.parse.quote(repo_name_utf8), repo_name)
498 assert_session_flash(response, flash_msg)
497 assert_session_flash(response, flash_msg)
499
498
500 # test if the repo was created in the database
499 # test if the repo was created in the database
501 new_repo = RepoModel().get_by_repo_name(repo_name)
500 new_repo = RepoModel().get_by_repo_name(repo_name)
502
501
503 assert new_repo.repo_name == repo_name
502 assert new_repo.repo_name == repo_name
504 assert new_repo.description == description
503 assert new_repo.description == description
505
504
506 # test if the repository is visible in the list ?
505 # test if the repository is visible in the list ?
507 response = self.app.get(
506 response = self.app.get(
508 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
507 h.route_path('repo_summary', repo_name=safe_str(repo_name)))
509 response.mustcontain(repo_name)
508 response.mustcontain(repo_name)
510 response.mustcontain(backend.alias)
509 response.mustcontain(backend.alias)
511
510
512 assert repo_on_filesystem(repo_name)
511 assert repo_on_filesystem(repo_name)
@@ -1,194 +1,193 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import os
20 import os
22 import pytest
21 import pytest
23
22
24 from rhodecode.apps._base import ADMIN_PREFIX
23 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
25 from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
27 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
28 from rhodecode.model.repo_group import RepoGroupModel
27 from rhodecode.model.repo_group import RepoGroupModel
29 from rhodecode.tests import (
28 from rhodecode.tests import (
30 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
29 assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
31 from rhodecode.tests.fixture import Fixture
30 from rhodecode.tests.fixture import Fixture
32
31
33 fixture = Fixture()
32 fixture = Fixture()
34
33
35
34
36 def route_path(name, params=None, **kwargs):
35 def route_path(name, params=None, **kwargs):
37 import urllib.request, urllib.parse, urllib.error
36 import urllib.request, urllib.parse, urllib.error
38
37
39 base_url = {
38 base_url = {
40 'repo_groups': ADMIN_PREFIX + '/repo_groups',
39 'repo_groups': ADMIN_PREFIX + '/repo_groups',
41 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
40 'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
42 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
41 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
43 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
42 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
44
43
45 }[name].format(**kwargs)
44 }[name].format(**kwargs)
46
45
47 if params:
46 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
48 return base_url
50
49
51
50
52 def _get_permission_for_user(user, repo):
51 def _get_permission_for_user(user, repo):
53 perm = UserRepoToPerm.query()\
52 perm = UserRepoToPerm.query()\
54 .filter(UserRepoToPerm.repository ==
53 .filter(UserRepoToPerm.repository ==
55 Repository.get_by_repo_name(repo))\
54 Repository.get_by_repo_name(repo))\
56 .filter(UserRepoToPerm.user == User.get_by_username(user))\
55 .filter(UserRepoToPerm.user == User.get_by_username(user))\
57 .all()
56 .all()
58 return perm
57 return perm
59
58
60
59
61 @pytest.mark.usefixtures("app")
60 @pytest.mark.usefixtures("app")
62 class TestAdminRepositoryGroups(object):
61 class TestAdminRepositoryGroups(object):
63
62
64 def test_show_repo_groups(self, autologin_user):
63 def test_show_repo_groups(self, autologin_user):
65 self.app.get(route_path('repo_groups'))
64 self.app.get(route_path('repo_groups'))
66
65
67 def test_show_repo_groups_data(self, autologin_user, xhr_header):
66 def test_show_repo_groups_data(self, autologin_user, xhr_header):
68 response = self.app.get(route_path(
67 response = self.app.get(route_path(
69 'repo_groups_data'), extra_environ=xhr_header)
68 'repo_groups_data'), extra_environ=xhr_header)
70
69
71 all_repo_groups = RepoGroup.query().count()
70 all_repo_groups = RepoGroup.query().count()
72 assert response.json['recordsTotal'] == all_repo_groups
71 assert response.json['recordsTotal'] == all_repo_groups
73
72
74 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
73 def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
75 response = self.app.get(route_path(
74 response = self.app.get(route_path(
76 'repo_groups_data', params={'search[value]': 'empty_search'}),
75 'repo_groups_data', params={'search[value]': 'empty_search'}),
77 extra_environ=xhr_header)
76 extra_environ=xhr_header)
78
77
79 all_repo_groups = RepoGroup.query().count()
78 all_repo_groups = RepoGroup.query().count()
80 assert response.json['recordsTotal'] == all_repo_groups
79 assert response.json['recordsTotal'] == all_repo_groups
81 assert response.json['recordsFiltered'] == 0
80 assert response.json['recordsFiltered'] == 0
82
81
83 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
82 def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
84 fixture.create_repo_group('test_repo_group')
83 fixture.create_repo_group('test_repo_group')
85 response = self.app.get(route_path(
84 response = self.app.get(route_path(
86 'repo_groups_data'), extra_environ=xhr_header)
85 'repo_groups_data'), extra_environ=xhr_header)
87 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
86 response.mustcontain('<a href=\\"/{}/_edit\\" title=\\"Edit\\">Edit</a>'.format('test_repo_group'))
88 fixture.destroy_repo_group('test_repo_group')
87 fixture.destroy_repo_group('test_repo_group')
89
88
90 def test_new(self, autologin_user):
89 def test_new(self, autologin_user):
91 self.app.get(route_path('repo_group_new'))
90 self.app.get(route_path('repo_group_new'))
92
91
93 def test_new_with_parent_group(self, autologin_user, user_util):
92 def test_new_with_parent_group(self, autologin_user, user_util):
94 gr = user_util.create_repo_group()
93 gr = user_util.create_repo_group()
95
94
96 self.app.get(route_path('repo_group_new'),
95 self.app.get(route_path('repo_group_new'),
97 params=dict(parent_group=gr.group_name))
96 params=dict(parent_group=gr.group_name))
98
97
99 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
98 def test_new_by_regular_user_no_permission(self, autologin_regular_user):
100 self.app.get(route_path('repo_group_new'), status=403)
99 self.app.get(route_path('repo_group_new'), status=403)
101
100
102 @pytest.mark.parametrize('repo_group_name', [
101 @pytest.mark.parametrize('repo_group_name', [
103 'git_repo',
102 'git_repo',
104 'git_repo_ąć',
103 'git_repo_ąć',
105 'hg_repo',
104 'hg_repo',
106 '12345',
105 '12345',
107 'hg_repo_ąć',
106 'hg_repo_ąć',
108 ])
107 ])
109 def test_create(self, autologin_user, repo_group_name, csrf_token):
108 def test_create(self, autologin_user, repo_group_name, csrf_token):
110 repo_group_name_unicode = repo_group_name.decode('utf8')
109 repo_group_name_unicode = repo_group_name.decode('utf8')
111 description = 'description for newly created repo group'
110 description = 'description for newly created repo group'
112
111
113 response = self.app.post(
112 response = self.app.post(
114 route_path('repo_group_create'),
113 route_path('repo_group_create'),
115 fixture._get_group_create_params(
114 fixture._get_group_create_params(
116 group_name=repo_group_name,
115 group_name=repo_group_name,
117 group_description=description,
116 group_description=description,
118 csrf_token=csrf_token))
117 csrf_token=csrf_token))
119
118
120 # run the check page that triggers the flash message
119 # run the check page that triggers the flash message
121 repo_gr_url = h.route_path(
120 repo_gr_url = h.route_path(
122 'repo_group_home', repo_group_name=repo_group_name)
121 'repo_group_home', repo_group_name=repo_group_name)
123
122
124 assert_session_flash(
123 assert_session_flash(
125 response,
124 response,
126 'Created repository group <a href="%s">%s</a>' % (
125 'Created repository group <a href="%s">%s</a>' % (
127 repo_gr_url, repo_group_name_unicode))
126 repo_gr_url, repo_group_name_unicode))
128
127
129 # # test if the repo group was created in the database
128 # # test if the repo group was created in the database
130 new_repo_group = RepoGroupModel()._get_repo_group(
129 new_repo_group = RepoGroupModel()._get_repo_group(
131 repo_group_name_unicode)
130 repo_group_name_unicode)
132 assert new_repo_group is not None
131 assert new_repo_group is not None
133
132
134 assert new_repo_group.group_name == repo_group_name_unicode
133 assert new_repo_group.group_name == repo_group_name_unicode
135 assert new_repo_group.group_description == description
134 assert new_repo_group.group_description == description
136
135
137 # test if the repository is visible in the list ?
136 # test if the repository is visible in the list ?
138 response = self.app.get(repo_gr_url)
137 response = self.app.get(repo_gr_url)
139 response.mustcontain(repo_group_name)
138 response.mustcontain(repo_group_name)
140
139
141 # test if the repository group was created on filesystem
140 # test if the repository group was created on filesystem
142 is_on_filesystem = os.path.isdir(
141 is_on_filesystem = os.path.isdir(
143 os.path.join(TESTS_TMP_PATH, repo_group_name))
142 os.path.join(TESTS_TMP_PATH, repo_group_name))
144 if not is_on_filesystem:
143 if not is_on_filesystem:
145 self.fail('no repo group %s in filesystem' % repo_group_name)
144 self.fail('no repo group %s in filesystem' % repo_group_name)
146
145
147 RepoGroupModel().delete(repo_group_name_unicode)
146 RepoGroupModel().delete(repo_group_name_unicode)
148 Session().commit()
147 Session().commit()
149
148
150 @pytest.mark.parametrize('repo_group_name', [
149 @pytest.mark.parametrize('repo_group_name', [
151 'git_repo',
150 'git_repo',
152 'git_repo_ąć',
151 'git_repo_ąć',
153 'hg_repo',
152 'hg_repo',
154 '12345',
153 '12345',
155 'hg_repo_ąć',
154 'hg_repo_ąć',
156 ])
155 ])
157 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
156 def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
158 parent_group = user_util.create_repo_group()
157 parent_group = user_util.create_repo_group()
159 parent_group_name = parent_group.group_name
158 parent_group_name = parent_group.group_name
160
159
161 expected_group_name = '{}/{}'.format(
160 expected_group_name = '{}/{}'.format(
162 parent_group_name, repo_group_name)
161 parent_group_name, repo_group_name)
163 expected_group_name_unicode = expected_group_name.decode('utf8')
162 expected_group_name_unicode = expected_group_name.decode('utf8')
164
163
165 try:
164 try:
166 response = self.app.post(
165 response = self.app.post(
167 route_path('repo_group_create'),
166 route_path('repo_group_create'),
168 fixture._get_group_create_params(
167 fixture._get_group_create_params(
169 group_name=repo_group_name,
168 group_name=repo_group_name,
170 group_parent_id=parent_group.group_id,
169 group_parent_id=parent_group.group_id,
171 group_description='Test desciption',
170 group_description='Test desciption',
172 csrf_token=csrf_token))
171 csrf_token=csrf_token))
173
172
174 assert_session_flash(
173 assert_session_flash(
175 response,
174 response,
176 u'Created repository group <a href="%s">%s</a>' % (
175 u'Created repository group <a href="%s">%s</a>' % (
177 h.route_path('repo_group_home',
176 h.route_path('repo_group_home',
178 repo_group_name=expected_group_name),
177 repo_group_name=expected_group_name),
179 expected_group_name_unicode))
178 expected_group_name_unicode))
180 finally:
179 finally:
181 RepoGroupModel().delete(expected_group_name_unicode)
180 RepoGroupModel().delete(expected_group_name_unicode)
182 Session().commit()
181 Session().commit()
183
182
184 def test_user_with_creation_permissions_cannot_create_subgroups(
183 def test_user_with_creation_permissions_cannot_create_subgroups(
185 self, autologin_regular_user, user_util):
184 self, autologin_regular_user, user_util):
186
185
187 user_util.grant_user_permission(
186 user_util.grant_user_permission(
188 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
187 TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
189 parent_group = user_util.create_repo_group()
188 parent_group = user_util.create_repo_group()
190 parent_group_id = parent_group.group_id
189 parent_group_id = parent_group.group_id
191 self.app.get(
190 self.app.get(
192 route_path('repo_group_new',
191 route_path('repo_group_new',
193 params=dict(parent_group=parent_group_id), ),
192 params=dict(parent_group=parent_group_id), ),
194 status=403)
193 status=403)
@@ -1,767 +1,766 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import mock
20 import mock
22 import pytest
21 import pytest
23
22
24 import rhodecode
23 import rhodecode
25 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.lib.utils2 import md5
25 from rhodecode.lib.utils2 import md5
27 from rhodecode.model.db import RhodeCodeUi
26 from rhodecode.model.db import RhodeCodeUi
28 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
29 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
28 from rhodecode.model.settings import SettingsModel, IssueTrackerSettingsModel
30 from rhodecode.tests import assert_session_flash
29 from rhodecode.tests import assert_session_flash
31 from rhodecode.tests.utils import AssertResponse
30 from rhodecode.tests.utils import AssertResponse
32
31
33
32
34 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
33 UPDATE_DATA_QUALNAME = 'rhodecode.model.update.UpdateModel.get_update_data'
35
34
36
35
37 def route_path(name, params=None, **kwargs):
36 def route_path(name, params=None, **kwargs):
38 import urllib.request, urllib.parse, urllib.error
37 import urllib.request, urllib.parse, urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
38 from rhodecode.apps._base import ADMIN_PREFIX
40
39
41 base_url = {
40 base_url = {
42
41
43 'admin_settings':
42 'admin_settings':
44 ADMIN_PREFIX +'/settings',
43 ADMIN_PREFIX +'/settings',
45 'admin_settings_update':
44 'admin_settings_update':
46 ADMIN_PREFIX + '/settings/update',
45 ADMIN_PREFIX + '/settings/update',
47 'admin_settings_global':
46 'admin_settings_global':
48 ADMIN_PREFIX + '/settings/global',
47 ADMIN_PREFIX + '/settings/global',
49 'admin_settings_global_update':
48 'admin_settings_global_update':
50 ADMIN_PREFIX + '/settings/global/update',
49 ADMIN_PREFIX + '/settings/global/update',
51 'admin_settings_vcs':
50 'admin_settings_vcs':
52 ADMIN_PREFIX + '/settings/vcs',
51 ADMIN_PREFIX + '/settings/vcs',
53 'admin_settings_vcs_update':
52 'admin_settings_vcs_update':
54 ADMIN_PREFIX + '/settings/vcs/update',
53 ADMIN_PREFIX + '/settings/vcs/update',
55 'admin_settings_vcs_svn_pattern_delete':
54 'admin_settings_vcs_svn_pattern_delete':
56 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
55 ADMIN_PREFIX + '/settings/vcs/svn_pattern_delete',
57 'admin_settings_mapping':
56 'admin_settings_mapping':
58 ADMIN_PREFIX + '/settings/mapping',
57 ADMIN_PREFIX + '/settings/mapping',
59 'admin_settings_mapping_update':
58 'admin_settings_mapping_update':
60 ADMIN_PREFIX + '/settings/mapping/update',
59 ADMIN_PREFIX + '/settings/mapping/update',
61 'admin_settings_visual':
60 'admin_settings_visual':
62 ADMIN_PREFIX + '/settings/visual',
61 ADMIN_PREFIX + '/settings/visual',
63 'admin_settings_visual_update':
62 'admin_settings_visual_update':
64 ADMIN_PREFIX + '/settings/visual/update',
63 ADMIN_PREFIX + '/settings/visual/update',
65 'admin_settings_issuetracker':
64 'admin_settings_issuetracker':
66 ADMIN_PREFIX + '/settings/issue-tracker',
65 ADMIN_PREFIX + '/settings/issue-tracker',
67 'admin_settings_issuetracker_update':
66 'admin_settings_issuetracker_update':
68 ADMIN_PREFIX + '/settings/issue-tracker/update',
67 ADMIN_PREFIX + '/settings/issue-tracker/update',
69 'admin_settings_issuetracker_test':
68 'admin_settings_issuetracker_test':
70 ADMIN_PREFIX + '/settings/issue-tracker/test',
69 ADMIN_PREFIX + '/settings/issue-tracker/test',
71 'admin_settings_issuetracker_delete':
70 'admin_settings_issuetracker_delete':
72 ADMIN_PREFIX + '/settings/issue-tracker/delete',
71 ADMIN_PREFIX + '/settings/issue-tracker/delete',
73 'admin_settings_email':
72 'admin_settings_email':
74 ADMIN_PREFIX + '/settings/email',
73 ADMIN_PREFIX + '/settings/email',
75 'admin_settings_email_update':
74 'admin_settings_email_update':
76 ADMIN_PREFIX + '/settings/email/update',
75 ADMIN_PREFIX + '/settings/email/update',
77 'admin_settings_hooks':
76 'admin_settings_hooks':
78 ADMIN_PREFIX + '/settings/hooks',
77 ADMIN_PREFIX + '/settings/hooks',
79 'admin_settings_hooks_update':
78 'admin_settings_hooks_update':
80 ADMIN_PREFIX + '/settings/hooks/update',
79 ADMIN_PREFIX + '/settings/hooks/update',
81 'admin_settings_hooks_delete':
80 'admin_settings_hooks_delete':
82 ADMIN_PREFIX + '/settings/hooks/delete',
81 ADMIN_PREFIX + '/settings/hooks/delete',
83 'admin_settings_search':
82 'admin_settings_search':
84 ADMIN_PREFIX + '/settings/search',
83 ADMIN_PREFIX + '/settings/search',
85 'admin_settings_labs':
84 'admin_settings_labs':
86 ADMIN_PREFIX + '/settings/labs',
85 ADMIN_PREFIX + '/settings/labs',
87 'admin_settings_labs_update':
86 'admin_settings_labs_update':
88 ADMIN_PREFIX + '/settings/labs/update',
87 ADMIN_PREFIX + '/settings/labs/update',
89
88
90 'admin_settings_sessions':
89 'admin_settings_sessions':
91 ADMIN_PREFIX + '/settings/sessions',
90 ADMIN_PREFIX + '/settings/sessions',
92 'admin_settings_sessions_cleanup':
91 'admin_settings_sessions_cleanup':
93 ADMIN_PREFIX + '/settings/sessions/cleanup',
92 ADMIN_PREFIX + '/settings/sessions/cleanup',
94 'admin_settings_system':
93 'admin_settings_system':
95 ADMIN_PREFIX + '/settings/system',
94 ADMIN_PREFIX + '/settings/system',
96 'admin_settings_system_update':
95 'admin_settings_system_update':
97 ADMIN_PREFIX + '/settings/system/updates',
96 ADMIN_PREFIX + '/settings/system/updates',
98 'admin_settings_open_source':
97 'admin_settings_open_source':
99 ADMIN_PREFIX + '/settings/open_source',
98 ADMIN_PREFIX + '/settings/open_source',
100
99
101
100
102 }[name].format(**kwargs)
101 }[name].format(**kwargs)
103
102
104 if params:
103 if params:
105 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
104 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
106 return base_url
105 return base_url
107
106
108
107
109 @pytest.mark.usefixtures('autologin_user', 'app')
108 @pytest.mark.usefixtures('autologin_user', 'app')
110 class TestAdminSettingsController(object):
109 class TestAdminSettingsController(object):
111
110
112 @pytest.mark.parametrize('urlname', [
111 @pytest.mark.parametrize('urlname', [
113 'admin_settings_vcs',
112 'admin_settings_vcs',
114 'admin_settings_mapping',
113 'admin_settings_mapping',
115 'admin_settings_global',
114 'admin_settings_global',
116 'admin_settings_visual',
115 'admin_settings_visual',
117 'admin_settings_email',
116 'admin_settings_email',
118 'admin_settings_hooks',
117 'admin_settings_hooks',
119 'admin_settings_search',
118 'admin_settings_search',
120 ])
119 ])
121 def test_simple_get(self, urlname):
120 def test_simple_get(self, urlname):
122 self.app.get(route_path(urlname))
121 self.app.get(route_path(urlname))
123
122
124 def test_create_custom_hook(self, csrf_token):
123 def test_create_custom_hook(self, csrf_token):
125 response = self.app.post(
124 response = self.app.post(
126 route_path('admin_settings_hooks_update'),
125 route_path('admin_settings_hooks_update'),
127 params={
126 params={
128 'new_hook_ui_key': 'test_hooks_1',
127 'new_hook_ui_key': 'test_hooks_1',
129 'new_hook_ui_value': 'cd /tmp',
128 'new_hook_ui_value': 'cd /tmp',
130 'csrf_token': csrf_token})
129 'csrf_token': csrf_token})
131
130
132 response = response.follow()
131 response = response.follow()
133 response.mustcontain('test_hooks_1')
132 response.mustcontain('test_hooks_1')
134 response.mustcontain('cd /tmp')
133 response.mustcontain('cd /tmp')
135
134
136 def test_create_custom_hook_delete(self, csrf_token):
135 def test_create_custom_hook_delete(self, csrf_token):
137 response = self.app.post(
136 response = self.app.post(
138 route_path('admin_settings_hooks_update'),
137 route_path('admin_settings_hooks_update'),
139 params={
138 params={
140 'new_hook_ui_key': 'test_hooks_2',
139 'new_hook_ui_key': 'test_hooks_2',
141 'new_hook_ui_value': 'cd /tmp2',
140 'new_hook_ui_value': 'cd /tmp2',
142 'csrf_token': csrf_token})
141 'csrf_token': csrf_token})
143
142
144 response = response.follow()
143 response = response.follow()
145 response.mustcontain('test_hooks_2')
144 response.mustcontain('test_hooks_2')
146 response.mustcontain('cd /tmp2')
145 response.mustcontain('cd /tmp2')
147
146
148 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
147 hook_id = SettingsModel().get_ui_by_key('test_hooks_2').ui_id
149
148
150 # delete
149 # delete
151 self.app.post(
150 self.app.post(
152 route_path('admin_settings_hooks_delete'),
151 route_path('admin_settings_hooks_delete'),
153 params={'hook_id': hook_id, 'csrf_token': csrf_token})
152 params={'hook_id': hook_id, 'csrf_token': csrf_token})
154 response = self.app.get(route_path('admin_settings_hooks'))
153 response = self.app.get(route_path('admin_settings_hooks'))
155 response.mustcontain(no=['test_hooks_2'])
154 response.mustcontain(no=['test_hooks_2'])
156 response.mustcontain(no=['cd /tmp2'])
155 response.mustcontain(no=['cd /tmp2'])
157
156
158
157
159 @pytest.mark.usefixtures('autologin_user', 'app')
158 @pytest.mark.usefixtures('autologin_user', 'app')
160 class TestAdminSettingsGlobal(object):
159 class TestAdminSettingsGlobal(object):
161
160
162 def test_pre_post_code_code_active(self, csrf_token):
161 def test_pre_post_code_code_active(self, csrf_token):
163 pre_code = 'rc-pre-code-187652122'
162 pre_code = 'rc-pre-code-187652122'
164 post_code = 'rc-postcode-98165231'
163 post_code = 'rc-postcode-98165231'
165
164
166 response = self.post_and_verify_settings({
165 response = self.post_and_verify_settings({
167 'rhodecode_pre_code': pre_code,
166 'rhodecode_pre_code': pre_code,
168 'rhodecode_post_code': post_code,
167 'rhodecode_post_code': post_code,
169 'csrf_token': csrf_token,
168 'csrf_token': csrf_token,
170 })
169 })
171
170
172 response = response.follow()
171 response = response.follow()
173 response.mustcontain(pre_code, post_code)
172 response.mustcontain(pre_code, post_code)
174
173
175 def test_pre_post_code_code_inactive(self, csrf_token):
174 def test_pre_post_code_code_inactive(self, csrf_token):
176 pre_code = 'rc-pre-code-187652122'
175 pre_code = 'rc-pre-code-187652122'
177 post_code = 'rc-postcode-98165231'
176 post_code = 'rc-postcode-98165231'
178 response = self.post_and_verify_settings({
177 response = self.post_and_verify_settings({
179 'rhodecode_pre_code': '',
178 'rhodecode_pre_code': '',
180 'rhodecode_post_code': '',
179 'rhodecode_post_code': '',
181 'csrf_token': csrf_token,
180 'csrf_token': csrf_token,
182 })
181 })
183
182
184 response = response.follow()
183 response = response.follow()
185 response.mustcontain(no=[pre_code, post_code])
184 response.mustcontain(no=[pre_code, post_code])
186
185
187 def test_captcha_activate(self, csrf_token):
186 def test_captcha_activate(self, csrf_token):
188 self.post_and_verify_settings({
187 self.post_and_verify_settings({
189 'rhodecode_captcha_private_key': '1234567890',
188 'rhodecode_captcha_private_key': '1234567890',
190 'rhodecode_captcha_public_key': '1234567890',
189 'rhodecode_captcha_public_key': '1234567890',
191 'csrf_token': csrf_token,
190 'csrf_token': csrf_token,
192 })
191 })
193
192
194 response = self.app.get(ADMIN_PREFIX + '/register')
193 response = self.app.get(ADMIN_PREFIX + '/register')
195 response.mustcontain('captcha')
194 response.mustcontain('captcha')
196
195
197 def test_captcha_deactivate(self, csrf_token):
196 def test_captcha_deactivate(self, csrf_token):
198 self.post_and_verify_settings({
197 self.post_and_verify_settings({
199 'rhodecode_captcha_private_key': '',
198 'rhodecode_captcha_private_key': '',
200 'rhodecode_captcha_public_key': '1234567890',
199 'rhodecode_captcha_public_key': '1234567890',
201 'csrf_token': csrf_token,
200 'csrf_token': csrf_token,
202 })
201 })
203
202
204 response = self.app.get(ADMIN_PREFIX + '/register')
203 response = self.app.get(ADMIN_PREFIX + '/register')
205 response.mustcontain(no=['captcha'])
204 response.mustcontain(no=['captcha'])
206
205
207 def test_title_change(self, csrf_token):
206 def test_title_change(self, csrf_token):
208 old_title = 'RhodeCode'
207 old_title = 'RhodeCode'
209
208
210 for new_title in ['Changed', 'Żółwik', old_title]:
209 for new_title in ['Changed', 'Żółwik', old_title]:
211 response = self.post_and_verify_settings({
210 response = self.post_and_verify_settings({
212 'rhodecode_title': new_title,
211 'rhodecode_title': new_title,
213 'csrf_token': csrf_token,
212 'csrf_token': csrf_token,
214 })
213 })
215
214
216 response = response.follow()
215 response = response.follow()
217 response.mustcontain(new_title)
216 response.mustcontain(new_title)
218
217
219 def post_and_verify_settings(self, settings):
218 def post_and_verify_settings(self, settings):
220 old_title = 'RhodeCode'
219 old_title = 'RhodeCode'
221 old_realm = 'RhodeCode authentication'
220 old_realm = 'RhodeCode authentication'
222 params = {
221 params = {
223 'rhodecode_title': old_title,
222 'rhodecode_title': old_title,
224 'rhodecode_realm': old_realm,
223 'rhodecode_realm': old_realm,
225 'rhodecode_pre_code': '',
224 'rhodecode_pre_code': '',
226 'rhodecode_post_code': '',
225 'rhodecode_post_code': '',
227 'rhodecode_captcha_private_key': '',
226 'rhodecode_captcha_private_key': '',
228 'rhodecode_captcha_public_key': '',
227 'rhodecode_captcha_public_key': '',
229 'rhodecode_create_personal_repo_group': False,
228 'rhodecode_create_personal_repo_group': False,
230 'rhodecode_personal_repo_group_pattern': '${username}',
229 'rhodecode_personal_repo_group_pattern': '${username}',
231 }
230 }
232 params.update(settings)
231 params.update(settings)
233 response = self.app.post(
232 response = self.app.post(
234 route_path('admin_settings_global_update'), params=params)
233 route_path('admin_settings_global_update'), params=params)
235
234
236 assert_session_flash(response, 'Updated application settings')
235 assert_session_flash(response, 'Updated application settings')
237 app_settings = SettingsModel().get_all_settings()
236 app_settings = SettingsModel().get_all_settings()
238 del settings['csrf_token']
237 del settings['csrf_token']
239 for key, value in settings.items():
238 for key, value in settings.items():
240 assert app_settings[key] == value
239 assert app_settings[key] == value
241
240
242 return response
241 return response
243
242
244
243
245 @pytest.mark.usefixtures('autologin_user', 'app')
244 @pytest.mark.usefixtures('autologin_user', 'app')
246 class TestAdminSettingsVcs(object):
245 class TestAdminSettingsVcs(object):
247
246
248 def test_contains_svn_default_patterns(self):
247 def test_contains_svn_default_patterns(self):
249 response = self.app.get(route_path('admin_settings_vcs'))
248 response = self.app.get(route_path('admin_settings_vcs'))
250 expected_patterns = [
249 expected_patterns = [
251 '/trunk',
250 '/trunk',
252 '/branches/*',
251 '/branches/*',
253 '/tags/*',
252 '/tags/*',
254 ]
253 ]
255 for pattern in expected_patterns:
254 for pattern in expected_patterns:
256 response.mustcontain(pattern)
255 response.mustcontain(pattern)
257
256
258 def test_add_new_svn_branch_and_tag_pattern(
257 def test_add_new_svn_branch_and_tag_pattern(
259 self, backend_svn, form_defaults, disable_sql_cache,
258 self, backend_svn, form_defaults, disable_sql_cache,
260 csrf_token):
259 csrf_token):
261 form_defaults.update({
260 form_defaults.update({
262 'new_svn_branch': '/exp/branches/*',
261 'new_svn_branch': '/exp/branches/*',
263 'new_svn_tag': '/important_tags/*',
262 'new_svn_tag': '/important_tags/*',
264 'csrf_token': csrf_token,
263 'csrf_token': csrf_token,
265 })
264 })
266
265
267 response = self.app.post(
266 response = self.app.post(
268 route_path('admin_settings_vcs_update'),
267 route_path('admin_settings_vcs_update'),
269 params=form_defaults, status=302)
268 params=form_defaults, status=302)
270 response = response.follow()
269 response = response.follow()
271
270
272 # Expect to find the new values on the page
271 # Expect to find the new values on the page
273 response.mustcontain('/exp/branches/*')
272 response.mustcontain('/exp/branches/*')
274 response.mustcontain('/important_tags/*')
273 response.mustcontain('/important_tags/*')
275
274
276 # Expect that those patterns are used to match branches and tags now
275 # Expect that those patterns are used to match branches and tags now
277 repo = backend_svn['svn-simple-layout'].scm_instance()
276 repo = backend_svn['svn-simple-layout'].scm_instance()
278 assert 'exp/branches/exp-sphinx-docs' in repo.branches
277 assert 'exp/branches/exp-sphinx-docs' in repo.branches
279 assert 'important_tags/v0.5' in repo.tags
278 assert 'important_tags/v0.5' in repo.tags
280
279
281 def test_add_same_svn_value_twice_shows_an_error_message(
280 def test_add_same_svn_value_twice_shows_an_error_message(
282 self, form_defaults, csrf_token, settings_util):
281 self, form_defaults, csrf_token, settings_util):
283 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
282 settings_util.create_rhodecode_ui('vcs_svn_branch', '/test')
284 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
283 settings_util.create_rhodecode_ui('vcs_svn_tag', '/test')
285
284
286 response = self.app.post(
285 response = self.app.post(
287 route_path('admin_settings_vcs_update'),
286 route_path('admin_settings_vcs_update'),
288 params={
287 params={
289 'paths_root_path': form_defaults['paths_root_path'],
288 'paths_root_path': form_defaults['paths_root_path'],
290 'new_svn_branch': '/test',
289 'new_svn_branch': '/test',
291 'new_svn_tag': '/test',
290 'new_svn_tag': '/test',
292 'csrf_token': csrf_token,
291 'csrf_token': csrf_token,
293 },
292 },
294 status=200)
293 status=200)
295
294
296 response.mustcontain("Pattern already exists")
295 response.mustcontain("Pattern already exists")
297 response.mustcontain("Some form inputs contain invalid data.")
296 response.mustcontain("Some form inputs contain invalid data.")
298
297
299 @pytest.mark.parametrize('section', [
298 @pytest.mark.parametrize('section', [
300 'vcs_svn_branch',
299 'vcs_svn_branch',
301 'vcs_svn_tag',
300 'vcs_svn_tag',
302 ])
301 ])
303 def test_delete_svn_patterns(
302 def test_delete_svn_patterns(
304 self, section, csrf_token, settings_util):
303 self, section, csrf_token, settings_util):
305 setting = settings_util.create_rhodecode_ui(
304 setting = settings_util.create_rhodecode_ui(
306 section, '/test_delete', cleanup=False)
305 section, '/test_delete', cleanup=False)
307
306
308 self.app.post(
307 self.app.post(
309 route_path('admin_settings_vcs_svn_pattern_delete'),
308 route_path('admin_settings_vcs_svn_pattern_delete'),
310 params={
309 params={
311 'delete_svn_pattern': setting.ui_id,
310 'delete_svn_pattern': setting.ui_id,
312 'csrf_token': csrf_token},
311 'csrf_token': csrf_token},
313 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
312 headers={'X-REQUESTED-WITH': 'XMLHttpRequest'})
314
313
315 @pytest.mark.parametrize('section', [
314 @pytest.mark.parametrize('section', [
316 'vcs_svn_branch',
315 'vcs_svn_branch',
317 'vcs_svn_tag',
316 'vcs_svn_tag',
318 ])
317 ])
319 def test_delete_svn_patterns_raises_404_when_no_xhr(
318 def test_delete_svn_patterns_raises_404_when_no_xhr(
320 self, section, csrf_token, settings_util):
319 self, section, csrf_token, settings_util):
321 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
320 setting = settings_util.create_rhodecode_ui(section, '/test_delete')
322
321
323 self.app.post(
322 self.app.post(
324 route_path('admin_settings_vcs_svn_pattern_delete'),
323 route_path('admin_settings_vcs_svn_pattern_delete'),
325 params={
324 params={
326 'delete_svn_pattern': setting.ui_id,
325 'delete_svn_pattern': setting.ui_id,
327 'csrf_token': csrf_token},
326 'csrf_token': csrf_token},
328 status=404)
327 status=404)
329
328
330 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
329 def test_extensions_hgsubversion(self, form_defaults, csrf_token):
331 form_defaults.update({
330 form_defaults.update({
332 'csrf_token': csrf_token,
331 'csrf_token': csrf_token,
333 'extensions_hgsubversion': 'True',
332 'extensions_hgsubversion': 'True',
334 })
333 })
335 response = self.app.post(
334 response = self.app.post(
336 route_path('admin_settings_vcs_update'),
335 route_path('admin_settings_vcs_update'),
337 params=form_defaults,
336 params=form_defaults,
338 status=302)
337 status=302)
339
338
340 response = response.follow()
339 response = response.follow()
341 extensions_input = (
340 extensions_input = (
342 '<input id="extensions_hgsubversion" '
341 '<input id="extensions_hgsubversion" '
343 'name="extensions_hgsubversion" type="checkbox" '
342 'name="extensions_hgsubversion" type="checkbox" '
344 'value="True" checked="checked" />')
343 'value="True" checked="checked" />')
345 response.mustcontain(extensions_input)
344 response.mustcontain(extensions_input)
346
345
347 def test_extensions_hgevolve(self, form_defaults, csrf_token):
346 def test_extensions_hgevolve(self, form_defaults, csrf_token):
348 form_defaults.update({
347 form_defaults.update({
349 'csrf_token': csrf_token,
348 'csrf_token': csrf_token,
350 'extensions_evolve': 'True',
349 'extensions_evolve': 'True',
351 })
350 })
352 response = self.app.post(
351 response = self.app.post(
353 route_path('admin_settings_vcs_update'),
352 route_path('admin_settings_vcs_update'),
354 params=form_defaults,
353 params=form_defaults,
355 status=302)
354 status=302)
356
355
357 response = response.follow()
356 response = response.follow()
358 extensions_input = (
357 extensions_input = (
359 '<input id="extensions_evolve" '
358 '<input id="extensions_evolve" '
360 'name="extensions_evolve" type="checkbox" '
359 'name="extensions_evolve" type="checkbox" '
361 'value="True" checked="checked" />')
360 'value="True" checked="checked" />')
362 response.mustcontain(extensions_input)
361 response.mustcontain(extensions_input)
363
362
364 def test_has_a_section_for_pull_request_settings(self):
363 def test_has_a_section_for_pull_request_settings(self):
365 response = self.app.get(route_path('admin_settings_vcs'))
364 response = self.app.get(route_path('admin_settings_vcs'))
366 response.mustcontain('Pull Request Settings')
365 response.mustcontain('Pull Request Settings')
367
366
368 def test_has_an_input_for_invalidation_of_inline_comments(self):
367 def test_has_an_input_for_invalidation_of_inline_comments(self):
369 response = self.app.get(route_path('admin_settings_vcs'))
368 response = self.app.get(route_path('admin_settings_vcs'))
370 assert_response = response.assert_response()
369 assert_response = response.assert_response()
371 assert_response.one_element_exists(
370 assert_response.one_element_exists(
372 '[name=rhodecode_use_outdated_comments]')
371 '[name=rhodecode_use_outdated_comments]')
373
372
374 @pytest.mark.parametrize('new_value', [True, False])
373 @pytest.mark.parametrize('new_value', [True, False])
375 def test_allows_to_change_invalidation_of_inline_comments(
374 def test_allows_to_change_invalidation_of_inline_comments(
376 self, form_defaults, csrf_token, new_value):
375 self, form_defaults, csrf_token, new_value):
377 setting_key = 'use_outdated_comments'
376 setting_key = 'use_outdated_comments'
378 setting = SettingsModel().create_or_update_setting(
377 setting = SettingsModel().create_or_update_setting(
379 setting_key, not new_value, 'bool')
378 setting_key, not new_value, 'bool')
380 Session().add(setting)
379 Session().add(setting)
381 Session().commit()
380 Session().commit()
382
381
383 form_defaults.update({
382 form_defaults.update({
384 'csrf_token': csrf_token,
383 'csrf_token': csrf_token,
385 'rhodecode_use_outdated_comments': str(new_value),
384 'rhodecode_use_outdated_comments': str(new_value),
386 })
385 })
387 response = self.app.post(
386 response = self.app.post(
388 route_path('admin_settings_vcs_update'),
387 route_path('admin_settings_vcs_update'),
389 params=form_defaults,
388 params=form_defaults,
390 status=302)
389 status=302)
391 response = response.follow()
390 response = response.follow()
392 setting = SettingsModel().get_setting_by_name(setting_key)
391 setting = SettingsModel().get_setting_by_name(setting_key)
393 assert setting.app_settings_value is new_value
392 assert setting.app_settings_value is new_value
394
393
395 @pytest.mark.parametrize('new_value', [True, False])
394 @pytest.mark.parametrize('new_value', [True, False])
396 def test_allows_to_change_hg_rebase_merge_strategy(
395 def test_allows_to_change_hg_rebase_merge_strategy(
397 self, form_defaults, csrf_token, new_value):
396 self, form_defaults, csrf_token, new_value):
398 setting_key = 'hg_use_rebase_for_merging'
397 setting_key = 'hg_use_rebase_for_merging'
399
398
400 form_defaults.update({
399 form_defaults.update({
401 'csrf_token': csrf_token,
400 'csrf_token': csrf_token,
402 'rhodecode_' + setting_key: str(new_value),
401 'rhodecode_' + setting_key: str(new_value),
403 })
402 })
404
403
405 with mock.patch.dict(
404 with mock.patch.dict(
406 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
405 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
407 self.app.post(
406 self.app.post(
408 route_path('admin_settings_vcs_update'),
407 route_path('admin_settings_vcs_update'),
409 params=form_defaults,
408 params=form_defaults,
410 status=302)
409 status=302)
411
410
412 setting = SettingsModel().get_setting_by_name(setting_key)
411 setting = SettingsModel().get_setting_by_name(setting_key)
413 assert setting.app_settings_value is new_value
412 assert setting.app_settings_value is new_value
414
413
415 @pytest.fixture()
414 @pytest.fixture()
416 def disable_sql_cache(self, request):
415 def disable_sql_cache(self, request):
417 patcher = mock.patch(
416 patcher = mock.patch(
418 'rhodecode.lib.caching_query.FromCache.process_query')
417 'rhodecode.lib.caching_query.FromCache.process_query')
419 request.addfinalizer(patcher.stop)
418 request.addfinalizer(patcher.stop)
420 patcher.start()
419 patcher.start()
421
420
422 @pytest.fixture()
421 @pytest.fixture()
423 def form_defaults(self):
422 def form_defaults(self):
424 from rhodecode.apps.admin.views.settings import AdminSettingsView
423 from rhodecode.apps.admin.views.settings import AdminSettingsView
425 return AdminSettingsView._form_defaults()
424 return AdminSettingsView._form_defaults()
426
425
427 # TODO: johbo: What we really want is to checkpoint before a test run and
426 # TODO: johbo: What we really want is to checkpoint before a test run and
428 # reset the session afterwards.
427 # reset the session afterwards.
429 @pytest.fixture(scope='class', autouse=True)
428 @pytest.fixture(scope='class', autouse=True)
430 def cleanup_settings(self, request, baseapp):
429 def cleanup_settings(self, request, baseapp):
431 ui_id = RhodeCodeUi.ui_id
430 ui_id = RhodeCodeUi.ui_id
432 original_ids = list(
431 original_ids = list(
433 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
432 r.ui_id for r in RhodeCodeUi.query().values(ui_id))
434
433
435 @request.addfinalizer
434 @request.addfinalizer
436 def cleanup():
435 def cleanup():
437 RhodeCodeUi.query().filter(
436 RhodeCodeUi.query().filter(
438 ui_id.notin_(original_ids)).delete(False)
437 ui_id.notin_(original_ids)).delete(False)
439
438
440
439
441 @pytest.mark.usefixtures('autologin_user', 'app')
440 @pytest.mark.usefixtures('autologin_user', 'app')
442 class TestLabsSettings(object):
441 class TestLabsSettings(object):
443 def test_get_settings_page_disabled(self):
442 def test_get_settings_page_disabled(self):
444 with mock.patch.dict(
443 with mock.patch.dict(
445 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
444 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
446
445
447 response = self.app.get(
446 response = self.app.get(
448 route_path('admin_settings_labs'), status=302)
447 route_path('admin_settings_labs'), status=302)
449
448
450 assert response.location.endswith(route_path('admin_settings'))
449 assert response.location.endswith(route_path('admin_settings'))
451
450
452 def test_get_settings_page_enabled(self):
451 def test_get_settings_page_enabled(self):
453 from rhodecode.apps.admin.views import settings
452 from rhodecode.apps.admin.views import settings
454 lab_settings = [
453 lab_settings = [
455 settings.LabSetting(
454 settings.LabSetting(
456 key='rhodecode_bool',
455 key='rhodecode_bool',
457 type='bool',
456 type='bool',
458 group='bool group',
457 group='bool group',
459 label='bool label',
458 label='bool label',
460 help='bool help'
459 help='bool help'
461 ),
460 ),
462 settings.LabSetting(
461 settings.LabSetting(
463 key='rhodecode_text',
462 key='rhodecode_text',
464 type='unicode',
463 type='unicode',
465 group='text group',
464 group='text group',
466 label='text label',
465 label='text label',
467 help='text help'
466 help='text help'
468 ),
467 ),
469 ]
468 ]
470 with mock.patch.dict(rhodecode.CONFIG,
469 with mock.patch.dict(rhodecode.CONFIG,
471 {'labs_settings_active': 'true'}):
470 {'labs_settings_active': 'true'}):
472 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
471 with mock.patch.object(settings, '_LAB_SETTINGS', lab_settings):
473 response = self.app.get(route_path('admin_settings_labs'))
472 response = self.app.get(route_path('admin_settings_labs'))
474
473
475 assert '<label>bool group:</label>' in response
474 assert '<label>bool group:</label>' in response
476 assert '<label for="rhodecode_bool">bool label</label>' in response
475 assert '<label for="rhodecode_bool">bool label</label>' in response
477 assert '<p class="help-block">bool help</p>' in response
476 assert '<p class="help-block">bool help</p>' in response
478 assert 'name="rhodecode_bool" type="checkbox"' in response
477 assert 'name="rhodecode_bool" type="checkbox"' in response
479
478
480 assert '<label>text group:</label>' in response
479 assert '<label>text group:</label>' in response
481 assert '<label for="rhodecode_text">text label</label>' in response
480 assert '<label for="rhodecode_text">text label</label>' in response
482 assert '<p class="help-block">text help</p>' in response
481 assert '<p class="help-block">text help</p>' in response
483 assert 'name="rhodecode_text" size="60" type="text"' in response
482 assert 'name="rhodecode_text" size="60" type="text"' in response
484
483
485
484
486 @pytest.mark.usefixtures('app')
485 @pytest.mark.usefixtures('app')
487 class TestOpenSourceLicenses(object):
486 class TestOpenSourceLicenses(object):
488
487
489 def test_records_are_displayed(self, autologin_user):
488 def test_records_are_displayed(self, autologin_user):
490 sample_licenses = [
489 sample_licenses = [
491 {
490 {
492 "license": [
491 "license": [
493 {
492 {
494 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
493 "fullName": "BSD 4-clause \"Original\" or \"Old\" License",
495 "shortName": "bsdOriginal",
494 "shortName": "bsdOriginal",
496 "spdxId": "BSD-4-Clause",
495 "spdxId": "BSD-4-Clause",
497 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
496 "url": "http://spdx.org/licenses/BSD-4-Clause.html"
498 }
497 }
499 ],
498 ],
500 "name": "python2.7-coverage-3.7.1"
499 "name": "python2.7-coverage-3.7.1"
501 },
500 },
502 {
501 {
503 "license": [
502 "license": [
504 {
503 {
505 "fullName": "MIT License",
504 "fullName": "MIT License",
506 "shortName": "mit",
505 "shortName": "mit",
507 "spdxId": "MIT",
506 "spdxId": "MIT",
508 "url": "http://spdx.org/licenses/MIT.html"
507 "url": "http://spdx.org/licenses/MIT.html"
509 }
508 }
510 ],
509 ],
511 "name": "python2.7-bootstrapped-pip-9.0.1"
510 "name": "python2.7-bootstrapped-pip-9.0.1"
512 },
511 },
513 ]
512 ]
514 read_licenses_patch = mock.patch(
513 read_licenses_patch = mock.patch(
515 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
514 'rhodecode.apps.admin.views.open_source_licenses.read_opensource_licenses',
516 return_value=sample_licenses)
515 return_value=sample_licenses)
517 with read_licenses_patch:
516 with read_licenses_patch:
518 response = self.app.get(
517 response = self.app.get(
519 route_path('admin_settings_open_source'), status=200)
518 route_path('admin_settings_open_source'), status=200)
520
519
521 assert_response = response.assert_response()
520 assert_response = response.assert_response()
522 assert_response.element_contains(
521 assert_response.element_contains(
523 '.panel-heading', 'Licenses of Third Party Packages')
522 '.panel-heading', 'Licenses of Third Party Packages')
524 for license_data in sample_licenses:
523 for license_data in sample_licenses:
525 response.mustcontain(license_data["license"][0]["spdxId"])
524 response.mustcontain(license_data["license"][0]["spdxId"])
526 assert_response.element_contains('.panel-body', license_data["name"])
525 assert_response.element_contains('.panel-body', license_data["name"])
527
526
528 def test_records_can_be_read(self, autologin_user):
527 def test_records_can_be_read(self, autologin_user):
529 response = self.app.get(
528 response = self.app.get(
530 route_path('admin_settings_open_source'), status=200)
529 route_path('admin_settings_open_source'), status=200)
531 assert_response = response.assert_response()
530 assert_response = response.assert_response()
532 assert_response.element_contains(
531 assert_response.element_contains(
533 '.panel-heading', 'Licenses of Third Party Packages')
532 '.panel-heading', 'Licenses of Third Party Packages')
534
533
535 def test_forbidden_when_normal_user(self, autologin_regular_user):
534 def test_forbidden_when_normal_user(self, autologin_regular_user):
536 self.app.get(
535 self.app.get(
537 route_path('admin_settings_open_source'), status=404)
536 route_path('admin_settings_open_source'), status=404)
538
537
539
538
540 @pytest.mark.usefixtures('app')
539 @pytest.mark.usefixtures('app')
541 class TestUserSessions(object):
540 class TestUserSessions(object):
542
541
543 def test_forbidden_when_normal_user(self, autologin_regular_user):
542 def test_forbidden_when_normal_user(self, autologin_regular_user):
544 self.app.get(route_path('admin_settings_sessions'), status=404)
543 self.app.get(route_path('admin_settings_sessions'), status=404)
545
544
546 def test_show_sessions_page(self, autologin_user):
545 def test_show_sessions_page(self, autologin_user):
547 response = self.app.get(route_path('admin_settings_sessions'), status=200)
546 response = self.app.get(route_path('admin_settings_sessions'), status=200)
548 response.mustcontain('file')
547 response.mustcontain('file')
549
548
550 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
549 def test_cleanup_old_sessions(self, autologin_user, csrf_token):
551
550
552 post_data = {
551 post_data = {
553 'csrf_token': csrf_token,
552 'csrf_token': csrf_token,
554 'expire_days': '60'
553 'expire_days': '60'
555 }
554 }
556 response = self.app.post(
555 response = self.app.post(
557 route_path('admin_settings_sessions_cleanup'), params=post_data,
556 route_path('admin_settings_sessions_cleanup'), params=post_data,
558 status=302)
557 status=302)
559 assert_session_flash(response, 'Cleaned up old sessions')
558 assert_session_flash(response, 'Cleaned up old sessions')
560
559
561
560
562 @pytest.mark.usefixtures('app')
561 @pytest.mark.usefixtures('app')
563 class TestAdminSystemInfo(object):
562 class TestAdminSystemInfo(object):
564
563
565 def test_forbidden_when_normal_user(self, autologin_regular_user):
564 def test_forbidden_when_normal_user(self, autologin_regular_user):
566 self.app.get(route_path('admin_settings_system'), status=404)
565 self.app.get(route_path('admin_settings_system'), status=404)
567
566
568 def test_system_info_page(self, autologin_user):
567 def test_system_info_page(self, autologin_user):
569 response = self.app.get(route_path('admin_settings_system'))
568 response = self.app.get(route_path('admin_settings_system'))
570 response.mustcontain('RhodeCode Community Edition, version {}'.format(
569 response.mustcontain('RhodeCode Community Edition, version {}'.format(
571 rhodecode.__version__))
570 rhodecode.__version__))
572
571
573 def test_system_update_new_version(self, autologin_user):
572 def test_system_update_new_version(self, autologin_user):
574 update_data = {
573 update_data = {
575 'versions': [
574 'versions': [
576 {
575 {
577 'version': '100.3.1415926535',
576 'version': '100.3.1415926535',
578 'general': 'The latest version we are ever going to ship'
577 'general': 'The latest version we are ever going to ship'
579 },
578 },
580 {
579 {
581 'version': '0.0.0',
580 'version': '0.0.0',
582 'general': 'The first version we ever shipped'
581 'general': 'The first version we ever shipped'
583 }
582 }
584 ]
583 ]
585 }
584 }
586 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
585 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
587 response = self.app.get(route_path('admin_settings_system_update'))
586 response = self.app.get(route_path('admin_settings_system_update'))
588 response.mustcontain('A <b>new version</b> is available')
587 response.mustcontain('A <b>new version</b> is available')
589
588
590 def test_system_update_nothing_new(self, autologin_user):
589 def test_system_update_nothing_new(self, autologin_user):
591 update_data = {
590 update_data = {
592 'versions': [
591 'versions': [
593 {
592 {
594 'version': '0.0.0',
593 'version': '0.0.0',
595 'general': 'The first version we ever shipped'
594 'general': 'The first version we ever shipped'
596 }
595 }
597 ]
596 ]
598 }
597 }
599 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
598 with mock.patch(UPDATE_DATA_QUALNAME, return_value=update_data):
600 response = self.app.get(route_path('admin_settings_system_update'))
599 response = self.app.get(route_path('admin_settings_system_update'))
601 response.mustcontain(
600 response.mustcontain(
602 'This instance is already running the <b>latest</b> stable version')
601 'This instance is already running the <b>latest</b> stable version')
603
602
604 def test_system_update_bad_response(self, autologin_user):
603 def test_system_update_bad_response(self, autologin_user):
605 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
604 with mock.patch(UPDATE_DATA_QUALNAME, side_effect=ValueError('foo')):
606 response = self.app.get(route_path('admin_settings_system_update'))
605 response = self.app.get(route_path('admin_settings_system_update'))
607 response.mustcontain(
606 response.mustcontain(
608 'Bad data sent from update server')
607 'Bad data sent from update server')
609
608
610
609
611 @pytest.mark.usefixtures("app")
610 @pytest.mark.usefixtures("app")
612 class TestAdminSettingsIssueTracker(object):
611 class TestAdminSettingsIssueTracker(object):
613 RC_PREFIX = 'rhodecode_'
612 RC_PREFIX = 'rhodecode_'
614 SHORT_PATTERN_KEY = 'issuetracker_pat_'
613 SHORT_PATTERN_KEY = 'issuetracker_pat_'
615 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
614 PATTERN_KEY = RC_PREFIX + SHORT_PATTERN_KEY
616 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
615 DESC_KEY = RC_PREFIX + 'issuetracker_desc_'
617
616
618 def test_issuetracker_index(self, autologin_user):
617 def test_issuetracker_index(self, autologin_user):
619 response = self.app.get(route_path('admin_settings_issuetracker'))
618 response = self.app.get(route_path('admin_settings_issuetracker'))
620 assert response.status_code == 200
619 assert response.status_code == 200
621
620
622 def test_add_empty_issuetracker_pattern(
621 def test_add_empty_issuetracker_pattern(
623 self, request, autologin_user, csrf_token):
622 self, request, autologin_user, csrf_token):
624 post_url = route_path('admin_settings_issuetracker_update')
623 post_url = route_path('admin_settings_issuetracker_update')
625 post_data = {
624 post_data = {
626 'csrf_token': csrf_token
625 'csrf_token': csrf_token
627 }
626 }
628 self.app.post(post_url, post_data, status=302)
627 self.app.post(post_url, post_data, status=302)
629
628
630 def test_add_issuetracker_pattern(
629 def test_add_issuetracker_pattern(
631 self, request, autologin_user, csrf_token):
630 self, request, autologin_user, csrf_token):
632 pattern = 'issuetracker_pat'
631 pattern = 'issuetracker_pat'
633 another_pattern = pattern+'1'
632 another_pattern = pattern+'1'
634 post_url = route_path('admin_settings_issuetracker_update')
633 post_url = route_path('admin_settings_issuetracker_update')
635 post_data = {
634 post_data = {
636 'new_pattern_pattern_0': pattern,
635 'new_pattern_pattern_0': pattern,
637 'new_pattern_url_0': 'http://url',
636 'new_pattern_url_0': 'http://url',
638 'new_pattern_prefix_0': 'prefix',
637 'new_pattern_prefix_0': 'prefix',
639 'new_pattern_description_0': 'description',
638 'new_pattern_description_0': 'description',
640 'new_pattern_pattern_1': another_pattern,
639 'new_pattern_pattern_1': another_pattern,
641 'new_pattern_url_1': 'https://url1',
640 'new_pattern_url_1': 'https://url1',
642 'new_pattern_prefix_1': 'prefix1',
641 'new_pattern_prefix_1': 'prefix1',
643 'new_pattern_description_1': 'description1',
642 'new_pattern_description_1': 'description1',
644 'csrf_token': csrf_token
643 'csrf_token': csrf_token
645 }
644 }
646 self.app.post(post_url, post_data, status=302)
645 self.app.post(post_url, post_data, status=302)
647 settings = SettingsModel().get_all_settings()
646 settings = SettingsModel().get_all_settings()
648 self.uid = md5(pattern)
647 self.uid = md5(pattern)
649 assert settings[self.PATTERN_KEY+self.uid] == pattern
648 assert settings[self.PATTERN_KEY+self.uid] == pattern
650 self.another_uid = md5(another_pattern)
649 self.another_uid = md5(another_pattern)
651 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
650 assert settings[self.PATTERN_KEY+self.another_uid] == another_pattern
652
651
653 @request.addfinalizer
652 @request.addfinalizer
654 def cleanup():
653 def cleanup():
655 defaults = SettingsModel().get_all_settings()
654 defaults = SettingsModel().get_all_settings()
656
655
657 entries = [name for name in defaults if (
656 entries = [name for name in defaults if (
658 (self.uid in name) or (self.another_uid) in name)]
657 (self.uid in name) or (self.another_uid) in name)]
659 start = len(self.RC_PREFIX)
658 start = len(self.RC_PREFIX)
660 for del_key in entries:
659 for del_key in entries:
661 # TODO: anderson: get_by_name needs name without prefix
660 # TODO: anderson: get_by_name needs name without prefix
662 entry = SettingsModel().get_setting_by_name(del_key[start:])
661 entry = SettingsModel().get_setting_by_name(del_key[start:])
663 Session().delete(entry)
662 Session().delete(entry)
664
663
665 Session().commit()
664 Session().commit()
666
665
667 def test_edit_issuetracker_pattern(
666 def test_edit_issuetracker_pattern(
668 self, autologin_user, backend, csrf_token, request):
667 self, autologin_user, backend, csrf_token, request):
669
668
670 old_pattern = 'issuetracker_pat1'
669 old_pattern = 'issuetracker_pat1'
671 old_uid = md5(old_pattern)
670 old_uid = md5(old_pattern)
672
671
673 post_url = route_path('admin_settings_issuetracker_update')
672 post_url = route_path('admin_settings_issuetracker_update')
674 post_data = {
673 post_data = {
675 'new_pattern_pattern_0': old_pattern,
674 'new_pattern_pattern_0': old_pattern,
676 'new_pattern_url_0': 'http://url',
675 'new_pattern_url_0': 'http://url',
677 'new_pattern_prefix_0': 'prefix',
676 'new_pattern_prefix_0': 'prefix',
678 'new_pattern_description_0': 'description',
677 'new_pattern_description_0': 'description',
679
678
680 'csrf_token': csrf_token
679 'csrf_token': csrf_token
681 }
680 }
682 self.app.post(post_url, post_data, status=302)
681 self.app.post(post_url, post_data, status=302)
683
682
684 new_pattern = 'issuetracker_pat1_edited'
683 new_pattern = 'issuetracker_pat1_edited'
685 self.new_uid = md5(new_pattern)
684 self.new_uid = md5(new_pattern)
686
685
687 post_url = route_path('admin_settings_issuetracker_update')
686 post_url = route_path('admin_settings_issuetracker_update')
688 post_data = {
687 post_data = {
689 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
688 'new_pattern_pattern_{}'.format(old_uid): new_pattern,
690 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
689 'new_pattern_url_{}'.format(old_uid): 'https://url_edited',
691 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
690 'new_pattern_prefix_{}'.format(old_uid): 'prefix_edited',
692 'new_pattern_description_{}'.format(old_uid): 'description_edited',
691 'new_pattern_description_{}'.format(old_uid): 'description_edited',
693 'uid': old_uid,
692 'uid': old_uid,
694 'csrf_token': csrf_token
693 'csrf_token': csrf_token
695 }
694 }
696 self.app.post(post_url, post_data, status=302)
695 self.app.post(post_url, post_data, status=302)
697
696
698 settings = SettingsModel().get_all_settings()
697 settings = SettingsModel().get_all_settings()
699 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
698 assert settings[self.PATTERN_KEY+self.new_uid] == new_pattern
700 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
699 assert settings[self.DESC_KEY + self.new_uid] == 'description_edited'
701 assert self.PATTERN_KEY+old_uid not in settings
700 assert self.PATTERN_KEY+old_uid not in settings
702
701
703 @request.addfinalizer
702 @request.addfinalizer
704 def cleanup():
703 def cleanup():
705 IssueTrackerSettingsModel().delete_entries(old_uid)
704 IssueTrackerSettingsModel().delete_entries(old_uid)
706 IssueTrackerSettingsModel().delete_entries(self.new_uid)
705 IssueTrackerSettingsModel().delete_entries(self.new_uid)
707
706
708 def test_replace_issuetracker_pattern_description(
707 def test_replace_issuetracker_pattern_description(
709 self, autologin_user, csrf_token, request, settings_util):
708 self, autologin_user, csrf_token, request, settings_util):
710 prefix = 'issuetracker'
709 prefix = 'issuetracker'
711 pattern = 'issuetracker_pat'
710 pattern = 'issuetracker_pat'
712 self.uid = md5(pattern)
711 self.uid = md5(pattern)
713 pattern_key = '_'.join([prefix, 'pat', self.uid])
712 pattern_key = '_'.join([prefix, 'pat', self.uid])
714 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
713 rc_pattern_key = '_'.join(['rhodecode', pattern_key])
715 desc_key = '_'.join([prefix, 'desc', self.uid])
714 desc_key = '_'.join([prefix, 'desc', self.uid])
716 rc_desc_key = '_'.join(['rhodecode', desc_key])
715 rc_desc_key = '_'.join(['rhodecode', desc_key])
717 new_description = 'new_description'
716 new_description = 'new_description'
718
717
719 settings_util.create_rhodecode_setting(
718 settings_util.create_rhodecode_setting(
720 pattern_key, pattern, 'unicode', cleanup=False)
719 pattern_key, pattern, 'unicode', cleanup=False)
721 settings_util.create_rhodecode_setting(
720 settings_util.create_rhodecode_setting(
722 desc_key, 'old description', 'unicode', cleanup=False)
721 desc_key, 'old description', 'unicode', cleanup=False)
723
722
724 post_url = route_path('admin_settings_issuetracker_update')
723 post_url = route_path('admin_settings_issuetracker_update')
725 post_data = {
724 post_data = {
726 'new_pattern_pattern_0': pattern,
725 'new_pattern_pattern_0': pattern,
727 'new_pattern_url_0': 'https://url',
726 'new_pattern_url_0': 'https://url',
728 'new_pattern_prefix_0': 'prefix',
727 'new_pattern_prefix_0': 'prefix',
729 'new_pattern_description_0': new_description,
728 'new_pattern_description_0': new_description,
730 'uid': self.uid,
729 'uid': self.uid,
731 'csrf_token': csrf_token
730 'csrf_token': csrf_token
732 }
731 }
733 self.app.post(post_url, post_data, status=302)
732 self.app.post(post_url, post_data, status=302)
734 settings = SettingsModel().get_all_settings()
733 settings = SettingsModel().get_all_settings()
735 assert settings[rc_pattern_key] == pattern
734 assert settings[rc_pattern_key] == pattern
736 assert settings[rc_desc_key] == new_description
735 assert settings[rc_desc_key] == new_description
737
736
738 @request.addfinalizer
737 @request.addfinalizer
739 def cleanup():
738 def cleanup():
740 IssueTrackerSettingsModel().delete_entries(self.uid)
739 IssueTrackerSettingsModel().delete_entries(self.uid)
741
740
742 def test_delete_issuetracker_pattern(
741 def test_delete_issuetracker_pattern(
743 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
742 self, autologin_user, backend, csrf_token, settings_util, xhr_header):
744
743
745 old_pattern = 'issuetracker_pat_deleted'
744 old_pattern = 'issuetracker_pat_deleted'
746 old_uid = md5(old_pattern)
745 old_uid = md5(old_pattern)
747
746
748 post_url = route_path('admin_settings_issuetracker_update')
747 post_url = route_path('admin_settings_issuetracker_update')
749 post_data = {
748 post_data = {
750 'new_pattern_pattern_0': old_pattern,
749 'new_pattern_pattern_0': old_pattern,
751 'new_pattern_url_0': 'http://url',
750 'new_pattern_url_0': 'http://url',
752 'new_pattern_prefix_0': 'prefix',
751 'new_pattern_prefix_0': 'prefix',
753 'new_pattern_description_0': 'description',
752 'new_pattern_description_0': 'description',
754
753
755 'csrf_token': csrf_token
754 'csrf_token': csrf_token
756 }
755 }
757 self.app.post(post_url, post_data, status=302)
756 self.app.post(post_url, post_data, status=302)
758
757
759 post_url = route_path('admin_settings_issuetracker_delete')
758 post_url = route_path('admin_settings_issuetracker_delete')
760 post_data = {
759 post_data = {
761 'uid': old_uid,
760 'uid': old_uid,
762 'csrf_token': csrf_token
761 'csrf_token': csrf_token
763 }
762 }
764 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
763 self.app.post(post_url, post_data, extra_environ=xhr_header, status=200)
765 settings = SettingsModel().get_all_settings()
764 settings = SettingsModel().get_all_settings()
766 assert self.PATTERN_KEY+old_uid not in settings
765 assert self.PATTERN_KEY+old_uid not in settings
767 assert self.DESC_KEY + old_uid not in settings
766 assert self.DESC_KEY + old_uid not in settings
@@ -1,170 +1,169 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22
21
23 from rhodecode.model.db import UserGroup, User
22 from rhodecode.model.db import UserGroup, User
24 from rhodecode.model.meta import Session
23 from rhodecode.model.meta import Session
25
24
26 from rhodecode.tests import (
25 from rhodecode.tests import (
27 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
27 from rhodecode.tests.fixture import Fixture
29
28
30 fixture = Fixture()
29 fixture = Fixture()
31
30
32
31
33 def route_path(name, params=None, **kwargs):
32 def route_path(name, params=None, **kwargs):
34 import urllib.request, urllib.parse, urllib.error
33 import urllib.request, urllib.parse, urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
34 from rhodecode.apps._base import ADMIN_PREFIX
36
35
37 base_url = {
36 base_url = {
38 'user_groups': ADMIN_PREFIX + '/user_groups',
37 'user_groups': ADMIN_PREFIX + '/user_groups',
39 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
38 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
40 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
39 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
41 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
40 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
42 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
41 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
43 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
42 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
44 }[name].format(**kwargs)
43 }[name].format(**kwargs)
45
44
46 if params:
45 if params:
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
46 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
48 return base_url
47 return base_url
49
48
50
49
51 class TestAdminUserGroupsView(TestController):
50 class TestAdminUserGroupsView(TestController):
52
51
53 def test_show_users(self):
52 def test_show_users(self):
54 self.log_user()
53 self.log_user()
55 self.app.get(route_path('user_groups'))
54 self.app.get(route_path('user_groups'))
56
55
57 def test_show_user_groups_data(self, xhr_header):
56 def test_show_user_groups_data(self, xhr_header):
58 self.log_user()
57 self.log_user()
59 response = self.app.get(route_path(
58 response = self.app.get(route_path(
60 'user_groups_data'), extra_environ=xhr_header)
59 'user_groups_data'), extra_environ=xhr_header)
61
60
62 all_user_groups = UserGroup.query().count()
61 all_user_groups = UserGroup.query().count()
63 assert response.json['recordsTotal'] == all_user_groups
62 assert response.json['recordsTotal'] == all_user_groups
64
63
65 def test_show_user_groups_data_filtered(self, xhr_header):
64 def test_show_user_groups_data_filtered(self, xhr_header):
66 self.log_user()
65 self.log_user()
67 response = self.app.get(route_path(
66 response = self.app.get(route_path(
68 'user_groups_data', params={'search[value]': 'empty_search'}),
67 'user_groups_data', params={'search[value]': 'empty_search'}),
69 extra_environ=xhr_header)
68 extra_environ=xhr_header)
70
69
71 all_user_groups = UserGroup.query().count()
70 all_user_groups = UserGroup.query().count()
72 assert response.json['recordsTotal'] == all_user_groups
71 assert response.json['recordsTotal'] == all_user_groups
73 assert response.json['recordsFiltered'] == 0
72 assert response.json['recordsFiltered'] == 0
74
73
75 def test_usergroup_escape(self, user_util, xhr_header):
74 def test_usergroup_escape(self, user_util, xhr_header):
76 self.log_user()
75 self.log_user()
77
76
78 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
77 xss_img = '<img src="/image1" onload="alert(\'Hello, World!\');">'
79 user = user_util.create_user()
78 user = user_util.create_user()
80 user.name = xss_img
79 user.name = xss_img
81 user.lastname = xss_img
80 user.lastname = xss_img
82 Session().add(user)
81 Session().add(user)
83 Session().commit()
82 Session().commit()
84
83
85 user_group = user_util.create_user_group()
84 user_group = user_util.create_user_group()
86
85
87 user_group.users_group_name = xss_img
86 user_group.users_group_name = xss_img
88 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
87 user_group.user_group_description = '<strong onload="alert();">DESC</strong>'
89
88
90 response = self.app.get(
89 response = self.app.get(
91 route_path('user_groups_data'), extra_environ=xhr_header)
90 route_path('user_groups_data'), extra_environ=xhr_header)
92
91
93 response.mustcontain(
92 response.mustcontain(
94 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
93 '&lt;strong onload=&#34;alert();&#34;&gt;DESC&lt;/strong&gt;')
95 response.mustcontain(
94 response.mustcontain(
96 '&lt;img src=&#34;/image1&#34; onload=&#34;'
95 '&lt;img src=&#34;/image1&#34; onload=&#34;'
97 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
96 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
98
97
99 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
98 def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
100 self.log_user()
99 self.log_user()
101 ug = user_util.create_user_group()
100 ug = user_util.create_user_group()
102 response = self.app.get(
101 response = self.app.get(
103 route_path('user_group_members_data', user_group_id=ug.users_group_id),
102 route_path('user_group_members_data', user_group_id=ug.users_group_id),
104 extra_environ=xhr_header)
103 extra_environ=xhr_header)
105
104
106 assert response.json == {'members': []}
105 assert response.json == {'members': []}
107
106
108 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
107 def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
109 self.log_user()
108 self.log_user()
110 members = [u.user_id for u in User.get_all()]
109 members = [u.user_id for u in User.get_all()]
111 ug = user_util.create_user_group(members=members)
110 ug = user_util.create_user_group(members=members)
112 response = self.app.get(
111 response = self.app.get(
113 route_path('user_group_members_data',
112 route_path('user_group_members_data',
114 user_group_id=ug.users_group_id),
113 user_group_id=ug.users_group_id),
115 extra_environ=xhr_header)
114 extra_environ=xhr_header)
116
115
117 assert len(response.json['members']) == len(members)
116 assert len(response.json['members']) == len(members)
118
117
119 def test_creation_page(self):
118 def test_creation_page(self):
120 self.log_user()
119 self.log_user()
121 self.app.get(route_path('user_groups_new'), status=200)
120 self.app.get(route_path('user_groups_new'), status=200)
122
121
123 def test_create(self):
122 def test_create(self):
124 from rhodecode.lib import helpers as h
123 from rhodecode.lib import helpers as h
125
124
126 self.log_user()
125 self.log_user()
127 users_group_name = 'test_user_group'
126 users_group_name = 'test_user_group'
128 response = self.app.post(route_path('user_groups_create'), {
127 response = self.app.post(route_path('user_groups_create'), {
129 'users_group_name': users_group_name,
128 'users_group_name': users_group_name,
130 'user_group_description': 'DESC',
129 'user_group_description': 'DESC',
131 'active': True,
130 'active': True,
132 'csrf_token': self.csrf_token})
131 'csrf_token': self.csrf_token})
133
132
134 user_group_id = UserGroup.get_by_group_name(
133 user_group_id = UserGroup.get_by_group_name(
135 users_group_name).users_group_id
134 users_group_name).users_group_id
136
135
137 user_group_link = h.link_to(
136 user_group_link = h.link_to(
138 users_group_name,
137 users_group_name,
139 route_path('edit_user_group', user_group_id=user_group_id))
138 route_path('edit_user_group', user_group_id=user_group_id))
140
139
141 assert_session_flash(
140 assert_session_flash(
142 response,
141 response,
143 'Created user group %s' % user_group_link)
142 'Created user group %s' % user_group_link)
144
143
145 fixture.destroy_user_group(users_group_name)
144 fixture.destroy_user_group(users_group_name)
146
145
147 def test_create_with_empty_name(self):
146 def test_create_with_empty_name(self):
148 self.log_user()
147 self.log_user()
149
148
150 response = self.app.post(route_path('user_groups_create'), {
149 response = self.app.post(route_path('user_groups_create'), {
151 'users_group_name': '',
150 'users_group_name': '',
152 'user_group_description': 'DESC',
151 'user_group_description': 'DESC',
153 'active': True,
152 'active': True,
154 'csrf_token': self.csrf_token}, status=200)
153 'csrf_token': self.csrf_token}, status=200)
155
154
156 response.mustcontain('Please enter a value')
155 response.mustcontain('Please enter a value')
157
156
158 def test_create_duplicate(self, user_util):
157 def test_create_duplicate(self, user_util):
159 self.log_user()
158 self.log_user()
160
159
161 user_group = user_util.create_user_group()
160 user_group = user_util.create_user_group()
162 duplicate_name = user_group.users_group_name
161 duplicate_name = user_group.users_group_name
163 response = self.app.post(route_path('user_groups_create'), {
162 response = self.app.post(route_path('user_groups_create'), {
164 'users_group_name': duplicate_name,
163 'users_group_name': duplicate_name,
165 'user_group_description': 'DESC',
164 'user_group_description': 'DESC',
166 'active': True,
165 'active': True,
167 'csrf_token': self.csrf_token}, status=200)
166 'csrf_token': self.csrf_token}, status=200)
168
167
169 response.mustcontain(
168 response.mustcontain(
170 'User group `{}` already exists'.format(duplicate_name))
169 'User group `{}` already exists'.format(duplicate_name))
@@ -1,794 +1,793 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22 from sqlalchemy.orm.exc import NoResultFound
21 from sqlalchemy.orm.exc import NoResultFound
23
22
24 from rhodecode.lib import auth
23 from rhodecode.lib import auth
25 from rhodecode.lib import helpers as h
24 from rhodecode.lib import helpers as h
26 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
25 from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
27 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
28 from rhodecode.model.user import UserModel
27 from rhodecode.model.user import UserModel
29
28
30 from rhodecode.tests import (
29 from rhodecode.tests import (
31 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
30 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
32 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.fixture import Fixture
33
32
34 fixture = Fixture()
33 fixture = Fixture()
35
34
36
35
37 def route_path(name, params=None, **kwargs):
36 def route_path(name, params=None, **kwargs):
38 import urllib.request, urllib.parse, urllib.error
37 import urllib.request, urllib.parse, urllib.error
39 from rhodecode.apps._base import ADMIN_PREFIX
38 from rhodecode.apps._base import ADMIN_PREFIX
40
39
41 base_url = {
40 base_url = {
42 'users':
41 'users':
43 ADMIN_PREFIX + '/users',
42 ADMIN_PREFIX + '/users',
44 'users_data':
43 'users_data':
45 ADMIN_PREFIX + '/users_data',
44 ADMIN_PREFIX + '/users_data',
46 'users_create':
45 'users_create':
47 ADMIN_PREFIX + '/users/create',
46 ADMIN_PREFIX + '/users/create',
48 'users_new':
47 'users_new':
49 ADMIN_PREFIX + '/users/new',
48 ADMIN_PREFIX + '/users/new',
50 'user_edit':
49 'user_edit':
51 ADMIN_PREFIX + '/users/{user_id}/edit',
50 ADMIN_PREFIX + '/users/{user_id}/edit',
52 'user_edit_advanced':
51 'user_edit_advanced':
53 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
52 ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
54 'user_edit_global_perms':
53 'user_edit_global_perms':
55 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
54 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
56 'user_edit_global_perms_update':
55 'user_edit_global_perms_update':
57 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
56 ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
58 'user_update':
57 'user_update':
59 ADMIN_PREFIX + '/users/{user_id}/update',
58 ADMIN_PREFIX + '/users/{user_id}/update',
60 'user_delete':
59 'user_delete':
61 ADMIN_PREFIX + '/users/{user_id}/delete',
60 ADMIN_PREFIX + '/users/{user_id}/delete',
62 'user_create_personal_repo_group':
61 'user_create_personal_repo_group':
63 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
62 ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
64
63
65 'edit_user_auth_tokens':
64 'edit_user_auth_tokens':
66 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
65 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
67 'edit_user_auth_tokens_add':
66 'edit_user_auth_tokens_add':
68 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
67 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
69 'edit_user_auth_tokens_delete':
68 'edit_user_auth_tokens_delete':
70 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
69 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
71
70
72 'edit_user_emails':
71 'edit_user_emails':
73 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
72 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
74 'edit_user_emails_add':
73 'edit_user_emails_add':
75 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
74 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
76 'edit_user_emails_delete':
75 'edit_user_emails_delete':
77 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
76 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
78
77
79 'edit_user_ips':
78 'edit_user_ips':
80 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
79 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
81 'edit_user_ips_add':
80 'edit_user_ips_add':
82 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
81 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
83 'edit_user_ips_delete':
82 'edit_user_ips_delete':
84 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
83 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
85
84
86 'edit_user_perms_summary':
85 'edit_user_perms_summary':
87 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
86 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
88 'edit_user_perms_summary_json':
87 'edit_user_perms_summary_json':
89 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
88 ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
90
89
91 'edit_user_audit_logs':
90 'edit_user_audit_logs':
92 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
91 ADMIN_PREFIX + '/users/{user_id}/edit/audit',
93
92
94 'edit_user_audit_logs_download':
93 'edit_user_audit_logs_download':
95 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
94 ADMIN_PREFIX + '/users/{user_id}/edit/audit/download',
96
95
97 }[name].format(**kwargs)
96 }[name].format(**kwargs)
98
97
99 if params:
98 if params:
100 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
99 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
101 return base_url
100 return base_url
102
101
103
102
104 class TestAdminUsersView(TestController):
103 class TestAdminUsersView(TestController):
105
104
106 def test_show_users(self):
105 def test_show_users(self):
107 self.log_user()
106 self.log_user()
108 self.app.get(route_path('users'))
107 self.app.get(route_path('users'))
109
108
110 def test_show_users_data(self, xhr_header):
109 def test_show_users_data(self, xhr_header):
111 self.log_user()
110 self.log_user()
112 response = self.app.get(route_path(
111 response = self.app.get(route_path(
113 'users_data'), extra_environ=xhr_header)
112 'users_data'), extra_environ=xhr_header)
114
113
115 all_users = User.query().filter(
114 all_users = User.query().filter(
116 User.username != User.DEFAULT_USER).count()
115 User.username != User.DEFAULT_USER).count()
117 assert response.json['recordsTotal'] == all_users
116 assert response.json['recordsTotal'] == all_users
118
117
119 def test_show_users_data_filtered(self, xhr_header):
118 def test_show_users_data_filtered(self, xhr_header):
120 self.log_user()
119 self.log_user()
121 response = self.app.get(route_path(
120 response = self.app.get(route_path(
122 'users_data', params={'search[value]': 'empty_search'}),
121 'users_data', params={'search[value]': 'empty_search'}),
123 extra_environ=xhr_header)
122 extra_environ=xhr_header)
124
123
125 all_users = User.query().filter(
124 all_users = User.query().filter(
126 User.username != User.DEFAULT_USER).count()
125 User.username != User.DEFAULT_USER).count()
127 assert response.json['recordsTotal'] == all_users
126 assert response.json['recordsTotal'] == all_users
128 assert response.json['recordsFiltered'] == 0
127 assert response.json['recordsFiltered'] == 0
129
128
130 def test_auth_tokens_default_user(self):
129 def test_auth_tokens_default_user(self):
131 self.log_user()
130 self.log_user()
132 user = User.get_default_user()
131 user = User.get_default_user()
133 response = self.app.get(
132 response = self.app.get(
134 route_path('edit_user_auth_tokens', user_id=user.user_id),
133 route_path('edit_user_auth_tokens', user_id=user.user_id),
135 status=302)
134 status=302)
136
135
137 def test_auth_tokens(self):
136 def test_auth_tokens(self):
138 self.log_user()
137 self.log_user()
139
138
140 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
139 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
141 user_id = user.user_id
140 user_id = user.user_id
142 auth_tokens = user.auth_tokens
141 auth_tokens = user.auth_tokens
143 response = self.app.get(
142 response = self.app.get(
144 route_path('edit_user_auth_tokens', user_id=user_id))
143 route_path('edit_user_auth_tokens', user_id=user_id))
145 for token in auth_tokens:
144 for token in auth_tokens:
146 response.mustcontain(token[:4])
145 response.mustcontain(token[:4])
147 response.mustcontain('never')
146 response.mustcontain('never')
148
147
149 @pytest.mark.parametrize("desc, lifetime", [
148 @pytest.mark.parametrize("desc, lifetime", [
150 ('forever', -1),
149 ('forever', -1),
151 ('5mins', 60*5),
150 ('5mins', 60*5),
152 ('30days', 60*60*24*30),
151 ('30days', 60*60*24*30),
153 ])
152 ])
154 def test_add_auth_token(self, desc, lifetime, user_util):
153 def test_add_auth_token(self, desc, lifetime, user_util):
155 self.log_user()
154 self.log_user()
156 user = user_util.create_user()
155 user = user_util.create_user()
157 user_id = user.user_id
156 user_id = user.user_id
158
157
159 response = self.app.post(
158 response = self.app.post(
160 route_path('edit_user_auth_tokens_add', user_id=user_id),
159 route_path('edit_user_auth_tokens_add', user_id=user_id),
161 {'description': desc, 'lifetime': lifetime,
160 {'description': desc, 'lifetime': lifetime,
162 'csrf_token': self.csrf_token})
161 'csrf_token': self.csrf_token})
163 assert_session_flash(response, 'Auth token successfully created')
162 assert_session_flash(response, 'Auth token successfully created')
164
163
165 response = response.follow()
164 response = response.follow()
166 user = User.get(user_id)
165 user = User.get(user_id)
167 for auth_token in user.auth_tokens:
166 for auth_token in user.auth_tokens:
168 response.mustcontain(auth_token[:4])
167 response.mustcontain(auth_token[:4])
169
168
170 def test_delete_auth_token(self, user_util):
169 def test_delete_auth_token(self, user_util):
171 self.log_user()
170 self.log_user()
172 user = user_util.create_user()
171 user = user_util.create_user()
173 user_id = user.user_id
172 user_id = user.user_id
174 keys = user.auth_tokens
173 keys = user.auth_tokens
175 assert 2 == len(keys)
174 assert 2 == len(keys)
176
175
177 response = self.app.post(
176 response = self.app.post(
178 route_path('edit_user_auth_tokens_add', user_id=user_id),
177 route_path('edit_user_auth_tokens_add', user_id=user_id),
179 {'description': 'desc', 'lifetime': -1,
178 {'description': 'desc', 'lifetime': -1,
180 'csrf_token': self.csrf_token})
179 'csrf_token': self.csrf_token})
181 assert_session_flash(response, 'Auth token successfully created')
180 assert_session_flash(response, 'Auth token successfully created')
182 response.follow()
181 response.follow()
183
182
184 # now delete our key
183 # now delete our key
185 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
184 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
186 assert 3 == len(keys)
185 assert 3 == len(keys)
187
186
188 response = self.app.post(
187 response = self.app.post(
189 route_path('edit_user_auth_tokens_delete', user_id=user_id),
188 route_path('edit_user_auth_tokens_delete', user_id=user_id),
190 {'del_auth_token': keys[0].user_api_key_id,
189 {'del_auth_token': keys[0].user_api_key_id,
191 'csrf_token': self.csrf_token})
190 'csrf_token': self.csrf_token})
192
191
193 assert_session_flash(response, 'Auth token successfully deleted')
192 assert_session_flash(response, 'Auth token successfully deleted')
194 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
193 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
195 assert 2 == len(keys)
194 assert 2 == len(keys)
196
195
197 def test_ips(self):
196 def test_ips(self):
198 self.log_user()
197 self.log_user()
199 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
198 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
200 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
199 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
201 response.mustcontain('All IP addresses are allowed')
200 response.mustcontain('All IP addresses are allowed')
202
201
203 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
202 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
204 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
203 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
205 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
204 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
206 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
205 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
207 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
206 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
208 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
207 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
209 ('127_bad_ip', 'foobar', 'foobar', True),
208 ('127_bad_ip', 'foobar', 'foobar', True),
210 ])
209 ])
211 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
210 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
212 self.log_user()
211 self.log_user()
213 user = user_util.create_user(username=test_name)
212 user = user_util.create_user(username=test_name)
214 user_id = user.user_id
213 user_id = user.user_id
215
214
216 response = self.app.post(
215 response = self.app.post(
217 route_path('edit_user_ips_add', user_id=user_id),
216 route_path('edit_user_ips_add', user_id=user_id),
218 params={'new_ip': ip, 'csrf_token': self.csrf_token})
217 params={'new_ip': ip, 'csrf_token': self.csrf_token})
219
218
220 if failure:
219 if failure:
221 assert_session_flash(
220 assert_session_flash(
222 response, 'Please enter a valid IPv4 or IpV6 address')
221 response, 'Please enter a valid IPv4 or IpV6 address')
223 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
222 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
224
223
225 response.mustcontain(no=[ip])
224 response.mustcontain(no=[ip])
226 response.mustcontain(no=[ip_range])
225 response.mustcontain(no=[ip_range])
227
226
228 else:
227 else:
229 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
228 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
230 response.mustcontain(ip)
229 response.mustcontain(ip)
231 response.mustcontain(ip_range)
230 response.mustcontain(ip_range)
232
231
233 def test_ips_delete(self, user_util):
232 def test_ips_delete(self, user_util):
234 self.log_user()
233 self.log_user()
235 user = user_util.create_user()
234 user = user_util.create_user()
236 user_id = user.user_id
235 user_id = user.user_id
237 ip = '127.0.0.1/32'
236 ip = '127.0.0.1/32'
238 ip_range = '127.0.0.1 - 127.0.0.1'
237 ip_range = '127.0.0.1 - 127.0.0.1'
239 new_ip = UserModel().add_extra_ip(user_id, ip)
238 new_ip = UserModel().add_extra_ip(user_id, ip)
240 Session().commit()
239 Session().commit()
241 new_ip_id = new_ip.ip_id
240 new_ip_id = new_ip.ip_id
242
241
243 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
242 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
244 response.mustcontain(ip)
243 response.mustcontain(ip)
245 response.mustcontain(ip_range)
244 response.mustcontain(ip_range)
246
245
247 self.app.post(
246 self.app.post(
248 route_path('edit_user_ips_delete', user_id=user_id),
247 route_path('edit_user_ips_delete', user_id=user_id),
249 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
248 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
250
249
251 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
250 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
252 response.mustcontain('All IP addresses are allowed')
251 response.mustcontain('All IP addresses are allowed')
253 response.mustcontain(no=[ip])
252 response.mustcontain(no=[ip])
254 response.mustcontain(no=[ip_range])
253 response.mustcontain(no=[ip_range])
255
254
256 def test_emails(self):
255 def test_emails(self):
257 self.log_user()
256 self.log_user()
258 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
257 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
259 response = self.app.get(
258 response = self.app.get(
260 route_path('edit_user_emails', user_id=user.user_id))
259 route_path('edit_user_emails', user_id=user.user_id))
261 response.mustcontain('No additional emails specified')
260 response.mustcontain('No additional emails specified')
262
261
263 def test_emails_add(self, user_util):
262 def test_emails_add(self, user_util):
264 self.log_user()
263 self.log_user()
265 user = user_util.create_user()
264 user = user_util.create_user()
266 user_id = user.user_id
265 user_id = user.user_id
267
266
268 self.app.post(
267 self.app.post(
269 route_path('edit_user_emails_add', user_id=user_id),
268 route_path('edit_user_emails_add', user_id=user_id),
270 params={'new_email': 'example@rhodecode.com',
269 params={'new_email': 'example@rhodecode.com',
271 'csrf_token': self.csrf_token})
270 'csrf_token': self.csrf_token})
272
271
273 response = self.app.get(
272 response = self.app.get(
274 route_path('edit_user_emails', user_id=user_id))
273 route_path('edit_user_emails', user_id=user_id))
275 response.mustcontain('example@rhodecode.com')
274 response.mustcontain('example@rhodecode.com')
276
275
277 def test_emails_add_existing_email(self, user_util, user_regular):
276 def test_emails_add_existing_email(self, user_util, user_regular):
278 existing_email = user_regular.email
277 existing_email = user_regular.email
279
278
280 self.log_user()
279 self.log_user()
281 user = user_util.create_user()
280 user = user_util.create_user()
282 user_id = user.user_id
281 user_id = user.user_id
283
282
284 response = self.app.post(
283 response = self.app.post(
285 route_path('edit_user_emails_add', user_id=user_id),
284 route_path('edit_user_emails_add', user_id=user_id),
286 params={'new_email': existing_email,
285 params={'new_email': existing_email,
287 'csrf_token': self.csrf_token})
286 'csrf_token': self.csrf_token})
288 assert_session_flash(
287 assert_session_flash(
289 response, 'This e-mail address is already taken')
288 response, 'This e-mail address is already taken')
290
289
291 response = self.app.get(
290 response = self.app.get(
292 route_path('edit_user_emails', user_id=user_id))
291 route_path('edit_user_emails', user_id=user_id))
293 response.mustcontain(no=[existing_email])
292 response.mustcontain(no=[existing_email])
294
293
295 def test_emails_delete(self, user_util):
294 def test_emails_delete(self, user_util):
296 self.log_user()
295 self.log_user()
297 user = user_util.create_user()
296 user = user_util.create_user()
298 user_id = user.user_id
297 user_id = user.user_id
299
298
300 self.app.post(
299 self.app.post(
301 route_path('edit_user_emails_add', user_id=user_id),
300 route_path('edit_user_emails_add', user_id=user_id),
302 params={'new_email': 'example@rhodecode.com',
301 params={'new_email': 'example@rhodecode.com',
303 'csrf_token': self.csrf_token})
302 'csrf_token': self.csrf_token})
304
303
305 response = self.app.get(
304 response = self.app.get(
306 route_path('edit_user_emails', user_id=user_id))
305 route_path('edit_user_emails', user_id=user_id))
307 response.mustcontain('example@rhodecode.com')
306 response.mustcontain('example@rhodecode.com')
308
307
309 user_email = UserEmailMap.query()\
308 user_email = UserEmailMap.query()\
310 .filter(UserEmailMap.email == 'example@rhodecode.com') \
309 .filter(UserEmailMap.email == 'example@rhodecode.com') \
311 .filter(UserEmailMap.user_id == user_id)\
310 .filter(UserEmailMap.user_id == user_id)\
312 .one()
311 .one()
313
312
314 del_email_id = user_email.email_id
313 del_email_id = user_email.email_id
315 self.app.post(
314 self.app.post(
316 route_path('edit_user_emails_delete', user_id=user_id),
315 route_path('edit_user_emails_delete', user_id=user_id),
317 params={'del_email_id': del_email_id,
316 params={'del_email_id': del_email_id,
318 'csrf_token': self.csrf_token})
317 'csrf_token': self.csrf_token})
319
318
320 response = self.app.get(
319 response = self.app.get(
321 route_path('edit_user_emails', user_id=user_id))
320 route_path('edit_user_emails', user_id=user_id))
322 response.mustcontain(no=['example@rhodecode.com'])
321 response.mustcontain(no=['example@rhodecode.com'])
323
322
324 def test_create(self, request, xhr_header):
323 def test_create(self, request, xhr_header):
325 self.log_user()
324 self.log_user()
326 username = 'newtestuser'
325 username = 'newtestuser'
327 password = 'test12'
326 password = 'test12'
328 password_confirmation = password
327 password_confirmation = password
329 name = 'name'
328 name = 'name'
330 lastname = 'lastname'
329 lastname = 'lastname'
331 email = 'mail@mail.com'
330 email = 'mail@mail.com'
332
331
333 self.app.get(route_path('users_new'))
332 self.app.get(route_path('users_new'))
334
333
335 response = self.app.post(route_path('users_create'), params={
334 response = self.app.post(route_path('users_create'), params={
336 'username': username,
335 'username': username,
337 'password': password,
336 'password': password,
338 'description': 'mr CTO',
337 'description': 'mr CTO',
339 'password_confirmation': password_confirmation,
338 'password_confirmation': password_confirmation,
340 'firstname': name,
339 'firstname': name,
341 'active': True,
340 'active': True,
342 'lastname': lastname,
341 'lastname': lastname,
343 'extern_name': 'rhodecode',
342 'extern_name': 'rhodecode',
344 'extern_type': 'rhodecode',
343 'extern_type': 'rhodecode',
345 'email': email,
344 'email': email,
346 'csrf_token': self.csrf_token,
345 'csrf_token': self.csrf_token,
347 })
346 })
348 user_link = h.link_to(
347 user_link = h.link_to(
349 username,
348 username,
350 route_path(
349 route_path(
351 'user_edit', user_id=User.get_by_username(username).user_id))
350 'user_edit', user_id=User.get_by_username(username).user_id))
352 assert_session_flash(response, 'Created user %s' % (user_link,))
351 assert_session_flash(response, 'Created user %s' % (user_link,))
353
352
354 @request.addfinalizer
353 @request.addfinalizer
355 def cleanup():
354 def cleanup():
356 fixture.destroy_user(username)
355 fixture.destroy_user(username)
357 Session().commit()
356 Session().commit()
358
357
359 new_user = User.query().filter(User.username == username).one()
358 new_user = User.query().filter(User.username == username).one()
360
359
361 assert new_user.username == username
360 assert new_user.username == username
362 assert auth.check_password(password, new_user.password)
361 assert auth.check_password(password, new_user.password)
363 assert new_user.name == name
362 assert new_user.name == name
364 assert new_user.lastname == lastname
363 assert new_user.lastname == lastname
365 assert new_user.email == email
364 assert new_user.email == email
366
365
367 response = self.app.get(route_path('users_data'),
366 response = self.app.get(route_path('users_data'),
368 extra_environ=xhr_header)
367 extra_environ=xhr_header)
369 response.mustcontain(username)
368 response.mustcontain(username)
370
369
371 def test_create_err(self):
370 def test_create_err(self):
372 self.log_user()
371 self.log_user()
373 username = 'new_user'
372 username = 'new_user'
374 password = ''
373 password = ''
375 name = 'name'
374 name = 'name'
376 lastname = 'lastname'
375 lastname = 'lastname'
377 email = 'errmail.com'
376 email = 'errmail.com'
378
377
379 self.app.get(route_path('users_new'))
378 self.app.get(route_path('users_new'))
380
379
381 response = self.app.post(route_path('users_create'), params={
380 response = self.app.post(route_path('users_create'), params={
382 'username': username,
381 'username': username,
383 'password': password,
382 'password': password,
384 'name': name,
383 'name': name,
385 'active': False,
384 'active': False,
386 'lastname': lastname,
385 'lastname': lastname,
387 'description': 'mr CTO',
386 'description': 'mr CTO',
388 'email': email,
387 'email': email,
389 'csrf_token': self.csrf_token,
388 'csrf_token': self.csrf_token,
390 })
389 })
391
390
392 msg = u'Username "%(username)s" is forbidden'
391 msg = u'Username "%(username)s" is forbidden'
393 msg = h.html_escape(msg % {'username': 'new_user'})
392 msg = h.html_escape(msg % {'username': 'new_user'})
394 response.mustcontain('<span class="error-message">%s</span>' % msg)
393 response.mustcontain('<span class="error-message">%s</span>' % msg)
395 response.mustcontain(
394 response.mustcontain(
396 '<span class="error-message">Please enter a value</span>')
395 '<span class="error-message">Please enter a value</span>')
397 response.mustcontain(
396 response.mustcontain(
398 '<span class="error-message">An email address must contain a'
397 '<span class="error-message">An email address must contain a'
399 ' single @</span>')
398 ' single @</span>')
400
399
401 def get_user():
400 def get_user():
402 Session().query(User).filter(User.username == username).one()
401 Session().query(User).filter(User.username == username).one()
403
402
404 with pytest.raises(NoResultFound):
403 with pytest.raises(NoResultFound):
405 get_user()
404 get_user()
406
405
407 def test_new(self):
406 def test_new(self):
408 self.log_user()
407 self.log_user()
409 self.app.get(route_path('users_new'))
408 self.app.get(route_path('users_new'))
410
409
411 @pytest.mark.parametrize("name, attrs", [
410 @pytest.mark.parametrize("name, attrs", [
412 ('firstname', {'firstname': 'new_username'}),
411 ('firstname', {'firstname': 'new_username'}),
413 ('lastname', {'lastname': 'new_username'}),
412 ('lastname', {'lastname': 'new_username'}),
414 ('admin', {'admin': True}),
413 ('admin', {'admin': True}),
415 ('admin', {'admin': False}),
414 ('admin', {'admin': False}),
416 ('extern_type', {'extern_type': 'ldap'}),
415 ('extern_type', {'extern_type': 'ldap'}),
417 ('extern_type', {'extern_type': None}),
416 ('extern_type', {'extern_type': None}),
418 ('extern_name', {'extern_name': 'test'}),
417 ('extern_name', {'extern_name': 'test'}),
419 ('extern_name', {'extern_name': None}),
418 ('extern_name', {'extern_name': None}),
420 ('active', {'active': False}),
419 ('active', {'active': False}),
421 ('active', {'active': True}),
420 ('active', {'active': True}),
422 ('email', {'email': 'some@email.com'}),
421 ('email', {'email': 'some@email.com'}),
423 ('language', {'language': 'de'}),
422 ('language', {'language': 'de'}),
424 ('language', {'language': 'en'}),
423 ('language', {'language': 'en'}),
425 ('description', {'description': 'hello CTO'}),
424 ('description', {'description': 'hello CTO'}),
426 # ('new_password', {'new_password': 'foobar123',
425 # ('new_password', {'new_password': 'foobar123',
427 # 'password_confirmation': 'foobar123'})
426 # 'password_confirmation': 'foobar123'})
428 ])
427 ])
429 def test_update(self, name, attrs, user_util):
428 def test_update(self, name, attrs, user_util):
430 self.log_user()
429 self.log_user()
431 usr = user_util.create_user(
430 usr = user_util.create_user(
432 password='qweqwe',
431 password='qweqwe',
433 email='testme@rhodecode.org',
432 email='testme@rhodecode.org',
434 extern_type='rhodecode',
433 extern_type='rhodecode',
435 extern_name='xxx',
434 extern_name='xxx',
436 )
435 )
437 user_id = usr.user_id
436 user_id = usr.user_id
438 Session().commit()
437 Session().commit()
439
438
440 params = usr.get_api_data()
439 params = usr.get_api_data()
441 cur_lang = params['language'] or 'en'
440 cur_lang = params['language'] or 'en'
442 params.update({
441 params.update({
443 'password_confirmation': '',
442 'password_confirmation': '',
444 'new_password': '',
443 'new_password': '',
445 'language': cur_lang,
444 'language': cur_lang,
446 'csrf_token': self.csrf_token,
445 'csrf_token': self.csrf_token,
447 })
446 })
448 params.update({'new_password': ''})
447 params.update({'new_password': ''})
449 params.update(attrs)
448 params.update(attrs)
450 if name == 'email':
449 if name == 'email':
451 params['emails'] = [attrs['email']]
450 params['emails'] = [attrs['email']]
452 elif name == 'extern_type':
451 elif name == 'extern_type':
453 # cannot update this via form, expected value is original one
452 # cannot update this via form, expected value is original one
454 params['extern_type'] = "rhodecode"
453 params['extern_type'] = "rhodecode"
455 elif name == 'extern_name':
454 elif name == 'extern_name':
456 # cannot update this via form, expected value is original one
455 # cannot update this via form, expected value is original one
457 params['extern_name'] = 'xxx'
456 params['extern_name'] = 'xxx'
458 # special case since this user is not
457 # special case since this user is not
459 # logged in yet his data is not filled
458 # logged in yet his data is not filled
460 # so we use creation data
459 # so we use creation data
461
460
462 response = self.app.post(
461 response = self.app.post(
463 route_path('user_update', user_id=usr.user_id), params)
462 route_path('user_update', user_id=usr.user_id), params)
464 assert response.status_int == 302
463 assert response.status_int == 302
465 assert_session_flash(response, 'User updated successfully')
464 assert_session_flash(response, 'User updated successfully')
466
465
467 updated_user = User.get(user_id)
466 updated_user = User.get(user_id)
468 updated_params = updated_user.get_api_data()
467 updated_params = updated_user.get_api_data()
469 updated_params.update({'password_confirmation': ''})
468 updated_params.update({'password_confirmation': ''})
470 updated_params.update({'new_password': ''})
469 updated_params.update({'new_password': ''})
471
470
472 del params['csrf_token']
471 del params['csrf_token']
473 assert params == updated_params
472 assert params == updated_params
474
473
475 def test_update_and_migrate_password(
474 def test_update_and_migrate_password(
476 self, autologin_user, real_crypto_backend, user_util):
475 self, autologin_user, real_crypto_backend, user_util):
477
476
478 user = user_util.create_user()
477 user = user_util.create_user()
479 temp_user = user.username
478 temp_user = user.username
480 user.password = auth._RhodeCodeCryptoSha256().hash_create(
479 user.password = auth._RhodeCodeCryptoSha256().hash_create(
481 b'test123')
480 b'test123')
482 Session().add(user)
481 Session().add(user)
483 Session().commit()
482 Session().commit()
484
483
485 params = user.get_api_data()
484 params = user.get_api_data()
486
485
487 params.update({
486 params.update({
488 'password_confirmation': 'qweqwe123',
487 'password_confirmation': 'qweqwe123',
489 'new_password': 'qweqwe123',
488 'new_password': 'qweqwe123',
490 'language': 'en',
489 'language': 'en',
491 'csrf_token': autologin_user.csrf_token,
490 'csrf_token': autologin_user.csrf_token,
492 })
491 })
493
492
494 response = self.app.post(
493 response = self.app.post(
495 route_path('user_update', user_id=user.user_id), params)
494 route_path('user_update', user_id=user.user_id), params)
496 assert response.status_int == 302
495 assert response.status_int == 302
497 assert_session_flash(response, 'User updated successfully')
496 assert_session_flash(response, 'User updated successfully')
498
497
499 # new password should be bcrypted, after log-in and transfer
498 # new password should be bcrypted, after log-in and transfer
500 user = User.get_by_username(temp_user)
499 user = User.get_by_username(temp_user)
501 assert user.password.startswith('$')
500 assert user.password.startswith('$')
502
501
503 updated_user = User.get_by_username(temp_user)
502 updated_user = User.get_by_username(temp_user)
504 updated_params = updated_user.get_api_data()
503 updated_params = updated_user.get_api_data()
505 updated_params.update({'password_confirmation': 'qweqwe123'})
504 updated_params.update({'password_confirmation': 'qweqwe123'})
506 updated_params.update({'new_password': 'qweqwe123'})
505 updated_params.update({'new_password': 'qweqwe123'})
507
506
508 del params['csrf_token']
507 del params['csrf_token']
509 assert params == updated_params
508 assert params == updated_params
510
509
511 def test_delete(self):
510 def test_delete(self):
512 self.log_user()
511 self.log_user()
513 username = 'newtestuserdeleteme'
512 username = 'newtestuserdeleteme'
514
513
515 fixture.create_user(name=username)
514 fixture.create_user(name=username)
516
515
517 new_user = Session().query(User)\
516 new_user = Session().query(User)\
518 .filter(User.username == username).one()
517 .filter(User.username == username).one()
519 response = self.app.post(
518 response = self.app.post(
520 route_path('user_delete', user_id=new_user.user_id),
519 route_path('user_delete', user_id=new_user.user_id),
521 params={'csrf_token': self.csrf_token})
520 params={'csrf_token': self.csrf_token})
522
521
523 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
522 assert_session_flash(response, 'Successfully deleted user `{}`'.format(username))
524
523
525 def test_delete_owner_of_repository(self, request, user_util):
524 def test_delete_owner_of_repository(self, request, user_util):
526 self.log_user()
525 self.log_user()
527 obj_name = 'test_repo'
526 obj_name = 'test_repo'
528 usr = user_util.create_user()
527 usr = user_util.create_user()
529 username = usr.username
528 username = usr.username
530 fixture.create_repo(obj_name, cur_user=usr.username)
529 fixture.create_repo(obj_name, cur_user=usr.username)
531
530
532 new_user = Session().query(User)\
531 new_user = Session().query(User)\
533 .filter(User.username == username).one()
532 .filter(User.username == username).one()
534 response = self.app.post(
533 response = self.app.post(
535 route_path('user_delete', user_id=new_user.user_id),
534 route_path('user_delete', user_id=new_user.user_id),
536 params={'csrf_token': self.csrf_token})
535 params={'csrf_token': self.csrf_token})
537
536
538 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
537 msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
539 'Switch owners or remove those repositories:%s' % (username, obj_name)
538 'Switch owners or remove those repositories:%s' % (username, obj_name)
540 assert_session_flash(response, msg)
539 assert_session_flash(response, msg)
541 fixture.destroy_repo(obj_name)
540 fixture.destroy_repo(obj_name)
542
541
543 def test_delete_owner_of_repository_detaching(self, request, user_util):
542 def test_delete_owner_of_repository_detaching(self, request, user_util):
544 self.log_user()
543 self.log_user()
545 obj_name = 'test_repo'
544 obj_name = 'test_repo'
546 usr = user_util.create_user(auto_cleanup=False)
545 usr = user_util.create_user(auto_cleanup=False)
547 username = usr.username
546 username = usr.username
548 fixture.create_repo(obj_name, cur_user=usr.username)
547 fixture.create_repo(obj_name, cur_user=usr.username)
549 Session().commit()
548 Session().commit()
550
549
551 new_user = Session().query(User)\
550 new_user = Session().query(User)\
552 .filter(User.username == username).one()
551 .filter(User.username == username).one()
553 response = self.app.post(
552 response = self.app.post(
554 route_path('user_delete', user_id=new_user.user_id),
553 route_path('user_delete', user_id=new_user.user_id),
555 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
554 params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
556
555
557 msg = 'Detached 1 repositories'
556 msg = 'Detached 1 repositories'
558 assert_session_flash(response, msg)
557 assert_session_flash(response, msg)
559 fixture.destroy_repo(obj_name)
558 fixture.destroy_repo(obj_name)
560
559
561 def test_delete_owner_of_repository_deleting(self, request, user_util):
560 def test_delete_owner_of_repository_deleting(self, request, user_util):
562 self.log_user()
561 self.log_user()
563 obj_name = 'test_repo'
562 obj_name = 'test_repo'
564 usr = user_util.create_user(auto_cleanup=False)
563 usr = user_util.create_user(auto_cleanup=False)
565 username = usr.username
564 username = usr.username
566 fixture.create_repo(obj_name, cur_user=usr.username)
565 fixture.create_repo(obj_name, cur_user=usr.username)
567
566
568 new_user = Session().query(User)\
567 new_user = Session().query(User)\
569 .filter(User.username == username).one()
568 .filter(User.username == username).one()
570 response = self.app.post(
569 response = self.app.post(
571 route_path('user_delete', user_id=new_user.user_id),
570 route_path('user_delete', user_id=new_user.user_id),
572 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
571 params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
573
572
574 msg = 'Deleted 1 repositories'
573 msg = 'Deleted 1 repositories'
575 assert_session_flash(response, msg)
574 assert_session_flash(response, msg)
576
575
577 def test_delete_owner_of_repository_group(self, request, user_util):
576 def test_delete_owner_of_repository_group(self, request, user_util):
578 self.log_user()
577 self.log_user()
579 obj_name = 'test_group'
578 obj_name = 'test_group'
580 usr = user_util.create_user()
579 usr = user_util.create_user()
581 username = usr.username
580 username = usr.username
582 fixture.create_repo_group(obj_name, cur_user=usr.username)
581 fixture.create_repo_group(obj_name, cur_user=usr.username)
583
582
584 new_user = Session().query(User)\
583 new_user = Session().query(User)\
585 .filter(User.username == username).one()
584 .filter(User.username == username).one()
586 response = self.app.post(
585 response = self.app.post(
587 route_path('user_delete', user_id=new_user.user_id),
586 route_path('user_delete', user_id=new_user.user_id),
588 params={'csrf_token': self.csrf_token})
587 params={'csrf_token': self.csrf_token})
589
588
590 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
589 msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
591 'Switch owners or remove those repository groups:%s' % (username, obj_name)
590 'Switch owners or remove those repository groups:%s' % (username, obj_name)
592 assert_session_flash(response, msg)
591 assert_session_flash(response, msg)
593 fixture.destroy_repo_group(obj_name)
592 fixture.destroy_repo_group(obj_name)
594
593
595 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
594 def test_delete_owner_of_repository_group_detaching(self, request, user_util):
596 self.log_user()
595 self.log_user()
597 obj_name = 'test_group'
596 obj_name = 'test_group'
598 usr = user_util.create_user(auto_cleanup=False)
597 usr = user_util.create_user(auto_cleanup=False)
599 username = usr.username
598 username = usr.username
600 fixture.create_repo_group(obj_name, cur_user=usr.username)
599 fixture.create_repo_group(obj_name, cur_user=usr.username)
601
600
602 new_user = Session().query(User)\
601 new_user = Session().query(User)\
603 .filter(User.username == username).one()
602 .filter(User.username == username).one()
604 response = self.app.post(
603 response = self.app.post(
605 route_path('user_delete', user_id=new_user.user_id),
604 route_path('user_delete', user_id=new_user.user_id),
606 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
605 params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
607
606
608 msg = 'Deleted 1 repository groups'
607 msg = 'Deleted 1 repository groups'
609 assert_session_flash(response, msg)
608 assert_session_flash(response, msg)
610
609
611 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
610 def test_delete_owner_of_repository_group_deleting(self, request, user_util):
612 self.log_user()
611 self.log_user()
613 obj_name = 'test_group'
612 obj_name = 'test_group'
614 usr = user_util.create_user(auto_cleanup=False)
613 usr = user_util.create_user(auto_cleanup=False)
615 username = usr.username
614 username = usr.username
616 fixture.create_repo_group(obj_name, cur_user=usr.username)
615 fixture.create_repo_group(obj_name, cur_user=usr.username)
617
616
618 new_user = Session().query(User)\
617 new_user = Session().query(User)\
619 .filter(User.username == username).one()
618 .filter(User.username == username).one()
620 response = self.app.post(
619 response = self.app.post(
621 route_path('user_delete', user_id=new_user.user_id),
620 route_path('user_delete', user_id=new_user.user_id),
622 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
621 params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
623
622
624 msg = 'Detached 1 repository groups'
623 msg = 'Detached 1 repository groups'
625 assert_session_flash(response, msg)
624 assert_session_flash(response, msg)
626 fixture.destroy_repo_group(obj_name)
625 fixture.destroy_repo_group(obj_name)
627
626
628 def test_delete_owner_of_user_group(self, request, user_util):
627 def test_delete_owner_of_user_group(self, request, user_util):
629 self.log_user()
628 self.log_user()
630 obj_name = 'test_user_group'
629 obj_name = 'test_user_group'
631 usr = user_util.create_user()
630 usr = user_util.create_user()
632 username = usr.username
631 username = usr.username
633 fixture.create_user_group(obj_name, cur_user=usr.username)
632 fixture.create_user_group(obj_name, cur_user=usr.username)
634
633
635 new_user = Session().query(User)\
634 new_user = Session().query(User)\
636 .filter(User.username == username).one()
635 .filter(User.username == username).one()
637 response = self.app.post(
636 response = self.app.post(
638 route_path('user_delete', user_id=new_user.user_id),
637 route_path('user_delete', user_id=new_user.user_id),
639 params={'csrf_token': self.csrf_token})
638 params={'csrf_token': self.csrf_token})
640
639
641 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
640 msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
642 'Switch owners or remove those user groups:%s' % (username, obj_name)
641 'Switch owners or remove those user groups:%s' % (username, obj_name)
643 assert_session_flash(response, msg)
642 assert_session_flash(response, msg)
644 fixture.destroy_user_group(obj_name)
643 fixture.destroy_user_group(obj_name)
645
644
646 def test_delete_owner_of_user_group_detaching(self, request, user_util):
645 def test_delete_owner_of_user_group_detaching(self, request, user_util):
647 self.log_user()
646 self.log_user()
648 obj_name = 'test_user_group'
647 obj_name = 'test_user_group'
649 usr = user_util.create_user(auto_cleanup=False)
648 usr = user_util.create_user(auto_cleanup=False)
650 username = usr.username
649 username = usr.username
651 fixture.create_user_group(obj_name, cur_user=usr.username)
650 fixture.create_user_group(obj_name, cur_user=usr.username)
652
651
653 new_user = Session().query(User)\
652 new_user = Session().query(User)\
654 .filter(User.username == username).one()
653 .filter(User.username == username).one()
655 try:
654 try:
656 response = self.app.post(
655 response = self.app.post(
657 route_path('user_delete', user_id=new_user.user_id),
656 route_path('user_delete', user_id=new_user.user_id),
658 params={'user_user_groups': 'detach',
657 params={'user_user_groups': 'detach',
659 'csrf_token': self.csrf_token})
658 'csrf_token': self.csrf_token})
660
659
661 msg = 'Detached 1 user groups'
660 msg = 'Detached 1 user groups'
662 assert_session_flash(response, msg)
661 assert_session_flash(response, msg)
663 finally:
662 finally:
664 fixture.destroy_user_group(obj_name)
663 fixture.destroy_user_group(obj_name)
665
664
666 def test_delete_owner_of_user_group_deleting(self, request, user_util):
665 def test_delete_owner_of_user_group_deleting(self, request, user_util):
667 self.log_user()
666 self.log_user()
668 obj_name = 'test_user_group'
667 obj_name = 'test_user_group'
669 usr = user_util.create_user(auto_cleanup=False)
668 usr = user_util.create_user(auto_cleanup=False)
670 username = usr.username
669 username = usr.username
671 fixture.create_user_group(obj_name, cur_user=usr.username)
670 fixture.create_user_group(obj_name, cur_user=usr.username)
672
671
673 new_user = Session().query(User)\
672 new_user = Session().query(User)\
674 .filter(User.username == username).one()
673 .filter(User.username == username).one()
675 response = self.app.post(
674 response = self.app.post(
676 route_path('user_delete', user_id=new_user.user_id),
675 route_path('user_delete', user_id=new_user.user_id),
677 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
676 params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
678
677
679 msg = 'Deleted 1 user groups'
678 msg = 'Deleted 1 user groups'
680 assert_session_flash(response, msg)
679 assert_session_flash(response, msg)
681
680
682 def test_edit(self, user_util):
681 def test_edit(self, user_util):
683 self.log_user()
682 self.log_user()
684 user = user_util.create_user()
683 user = user_util.create_user()
685 self.app.get(route_path('user_edit', user_id=user.user_id))
684 self.app.get(route_path('user_edit', user_id=user.user_id))
686
685
687 def test_edit_default_user_redirect(self):
686 def test_edit_default_user_redirect(self):
688 self.log_user()
687 self.log_user()
689 user = User.get_default_user()
688 user = User.get_default_user()
690 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
689 self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
691
690
692 @pytest.mark.parametrize(
691 @pytest.mark.parametrize(
693 'repo_create, repo_create_write, user_group_create, repo_group_create,'
692 'repo_create, repo_create_write, user_group_create, repo_group_create,'
694 'fork_create, inherit_default_permissions, expect_error,'
693 'fork_create, inherit_default_permissions, expect_error,'
695 'expect_form_error', [
694 'expect_form_error', [
696 ('hg.create.none', 'hg.create.write_on_repogroup.false',
695 ('hg.create.none', 'hg.create.write_on_repogroup.false',
697 'hg.usergroup.create.false', 'hg.repogroup.create.false',
696 'hg.usergroup.create.false', 'hg.repogroup.create.false',
698 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
697 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
699 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
698 ('hg.create.repository', 'hg.create.write_on_repogroup.false',
700 'hg.usergroup.create.false', 'hg.repogroup.create.false',
699 'hg.usergroup.create.false', 'hg.repogroup.create.false',
701 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
700 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
702 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
701 ('hg.create.repository', 'hg.create.write_on_repogroup.true',
703 'hg.usergroup.create.true', 'hg.repogroup.create.true',
702 'hg.usergroup.create.true', 'hg.repogroup.create.true',
704 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
703 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
705 False),
704 False),
706 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
705 ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
707 'hg.usergroup.create.true', 'hg.repogroup.create.true',
706 'hg.usergroup.create.true', 'hg.repogroup.create.true',
708 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
707 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
709 True),
708 True),
710 ('', '', '', '', '', '', True, False),
709 ('', '', '', '', '', '', True, False),
711 ])
710 ])
712 def test_global_perms_on_user(
711 def test_global_perms_on_user(
713 self, repo_create, repo_create_write, user_group_create,
712 self, repo_create, repo_create_write, user_group_create,
714 repo_group_create, fork_create, expect_error, expect_form_error,
713 repo_group_create, fork_create, expect_error, expect_form_error,
715 inherit_default_permissions, user_util):
714 inherit_default_permissions, user_util):
716 self.log_user()
715 self.log_user()
717 user = user_util.create_user()
716 user = user_util.create_user()
718 uid = user.user_id
717 uid = user.user_id
719
718
720 # ENABLE REPO CREATE ON A GROUP
719 # ENABLE REPO CREATE ON A GROUP
721 perm_params = {
720 perm_params = {
722 'inherit_default_permissions': False,
721 'inherit_default_permissions': False,
723 'default_repo_create': repo_create,
722 'default_repo_create': repo_create,
724 'default_repo_create_on_write': repo_create_write,
723 'default_repo_create_on_write': repo_create_write,
725 'default_user_group_create': user_group_create,
724 'default_user_group_create': user_group_create,
726 'default_repo_group_create': repo_group_create,
725 'default_repo_group_create': repo_group_create,
727 'default_fork_create': fork_create,
726 'default_fork_create': fork_create,
728 'default_inherit_default_permissions': inherit_default_permissions,
727 'default_inherit_default_permissions': inherit_default_permissions,
729 'csrf_token': self.csrf_token,
728 'csrf_token': self.csrf_token,
730 }
729 }
731 response = self.app.post(
730 response = self.app.post(
732 route_path('user_edit_global_perms_update', user_id=uid),
731 route_path('user_edit_global_perms_update', user_id=uid),
733 params=perm_params)
732 params=perm_params)
734
733
735 if expect_form_error:
734 if expect_form_error:
736 assert response.status_int == 200
735 assert response.status_int == 200
737 response.mustcontain('Value must be one of')
736 response.mustcontain('Value must be one of')
738 else:
737 else:
739 if expect_error:
738 if expect_error:
740 msg = 'An error occurred during permissions saving'
739 msg = 'An error occurred during permissions saving'
741 else:
740 else:
742 msg = 'User global permissions updated successfully'
741 msg = 'User global permissions updated successfully'
743 ug = User.get(uid)
742 ug = User.get(uid)
744 del perm_params['inherit_default_permissions']
743 del perm_params['inherit_default_permissions']
745 del perm_params['csrf_token']
744 del perm_params['csrf_token']
746 assert perm_params == ug.get_default_perms()
745 assert perm_params == ug.get_default_perms()
747 assert_session_flash(response, msg)
746 assert_session_flash(response, msg)
748
747
749 def test_global_permissions_initial_values(self, user_util):
748 def test_global_permissions_initial_values(self, user_util):
750 self.log_user()
749 self.log_user()
751 user = user_util.create_user()
750 user = user_util.create_user()
752 uid = user.user_id
751 uid = user.user_id
753 response = self.app.get(
752 response = self.app.get(
754 route_path('user_edit_global_perms', user_id=uid))
753 route_path('user_edit_global_perms', user_id=uid))
755 default_user = User.get_default_user()
754 default_user = User.get_default_user()
756 default_permissions = default_user.get_default_perms()
755 default_permissions = default_user.get_default_perms()
757 assert_response = response.assert_response()
756 assert_response = response.assert_response()
758 expected_permissions = (
757 expected_permissions = (
759 'default_repo_create', 'default_repo_create_on_write',
758 'default_repo_create', 'default_repo_create_on_write',
760 'default_fork_create', 'default_repo_group_create',
759 'default_fork_create', 'default_repo_group_create',
761 'default_user_group_create', 'default_inherit_default_permissions')
760 'default_user_group_create', 'default_inherit_default_permissions')
762 for permission in expected_permissions:
761 for permission in expected_permissions:
763 css_selector = '[name={}][checked=checked]'.format(permission)
762 css_selector = '[name={}][checked=checked]'.format(permission)
764 element = assert_response.get_element(css_selector)
763 element = assert_response.get_element(css_selector)
765 assert element.value == default_permissions[permission]
764 assert element.value == default_permissions[permission]
766
765
767 def test_perms_summary_page(self):
766 def test_perms_summary_page(self):
768 user = self.log_user()
767 user = self.log_user()
769 response = self.app.get(
768 response = self.app.get(
770 route_path('edit_user_perms_summary', user_id=user['user_id']))
769 route_path('edit_user_perms_summary', user_id=user['user_id']))
771 for repo in Repository.query().all():
770 for repo in Repository.query().all():
772 response.mustcontain(repo.repo_name)
771 response.mustcontain(repo.repo_name)
773
772
774 def test_perms_summary_page_json(self):
773 def test_perms_summary_page_json(self):
775 user = self.log_user()
774 user = self.log_user()
776 response = self.app.get(
775 response = self.app.get(
777 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
776 route_path('edit_user_perms_summary_json', user_id=user['user_id']))
778 for repo in Repository.query().all():
777 for repo in Repository.query().all():
779 response.mustcontain(repo.repo_name)
778 response.mustcontain(repo.repo_name)
780
779
781 def test_audit_log_page(self):
780 def test_audit_log_page(self):
782 user = self.log_user()
781 user = self.log_user()
783 self.app.get(
782 self.app.get(
784 route_path('edit_user_audit_logs', user_id=user['user_id']))
783 route_path('edit_user_audit_logs', user_id=user['user_id']))
785
784
786 def test_audit_log_page_download(self):
785 def test_audit_log_page_download(self):
787 user = self.log_user()
786 user = self.log_user()
788 user_id = user['user_id']
787 user_id = user['user_id']
789 response = self.app.get(
788 response = self.app.get(
790 route_path('edit_user_audit_logs_download', user_id=user_id))
789 route_path('edit_user_audit_logs_download', user_id=user_id))
791
790
792 assert response.content_disposition == \
791 assert response.content_disposition == \
793 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
792 'attachment; filename=user_{}_audit_logs.json'.format(user_id)
794 assert response.content_type == "application/json"
793 assert response.content_type == "application/json"
@@ -1,176 +1,175 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import pytest
20 import pytest
22
21
23 from rhodecode.model.db import User, UserSshKeys
22 from rhodecode.model.db import User, UserSshKeys
24
23
25 from rhodecode.tests import TestController, assert_session_flash
24 from rhodecode.tests import TestController, assert_session_flash
26 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
27
26
28 fixture = Fixture()
27 fixture = Fixture()
29
28
30
29
31 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
32 import urllib.request, urllib.parse, urllib.error
31 import urllib.request, urllib.parse, urllib.error
33 from rhodecode.apps._base import ADMIN_PREFIX
32 from rhodecode.apps._base import ADMIN_PREFIX
34
33
35 base_url = {
34 base_url = {
36 'edit_user_ssh_keys':
35 'edit_user_ssh_keys':
37 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
36 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
38 'edit_user_ssh_keys_generate_keypair':
37 'edit_user_ssh_keys_generate_keypair':
39 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
38 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
40 'edit_user_ssh_keys_add':
39 'edit_user_ssh_keys_add':
41 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
40 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
42 'edit_user_ssh_keys_delete':
41 'edit_user_ssh_keys_delete':
43 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
42 ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
44
43
45 }[name].format(**kwargs)
44 }[name].format(**kwargs)
46
45
47 if params:
46 if params:
48 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
47 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
49 return base_url
48 return base_url
50
49
51
50
52 class TestAdminUsersSshKeysView(TestController):
51 class TestAdminUsersSshKeysView(TestController):
53 INVALID_KEY = """\
52 INVALID_KEY = """\
54 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
53 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
55 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
54 LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
56 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
55 n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
57 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
56 cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
58 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
57 jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
59 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
58 qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
60 your_email@example.com
59 your_email@example.com
61 """
60 """
62 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
61 VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
63 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
62 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
64 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
63 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
65 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
64 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
66 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
65 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
67 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
66 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
68 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
67 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
69 'your_email@example.com'
68 'your_email@example.com'
70 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
69 FINGERPRINT = 'MD5:01:4f:ad:29:22:6e:01:37:c9:d2:52:26:52:b0:2d:93'
71
70
72 def test_ssh_keys_default_user(self):
71 def test_ssh_keys_default_user(self):
73 self.log_user()
72 self.log_user()
74 user = User.get_default_user()
73 user = User.get_default_user()
75 self.app.get(
74 self.app.get(
76 route_path('edit_user_ssh_keys', user_id=user.user_id),
75 route_path('edit_user_ssh_keys', user_id=user.user_id),
77 status=302)
76 status=302)
78
77
79 def test_add_ssh_key_error(self, user_util):
78 def test_add_ssh_key_error(self, user_util):
80 self.log_user()
79 self.log_user()
81 user = user_util.create_user()
80 user = user_util.create_user()
82 user_id = user.user_id
81 user_id = user.user_id
83
82
84 key_data = self.INVALID_KEY
83 key_data = self.INVALID_KEY
85
84
86 desc = 'MY SSH KEY'
85 desc = 'MY SSH KEY'
87 response = self.app.post(
86 response = self.app.post(
88 route_path('edit_user_ssh_keys_add', user_id=user_id),
87 route_path('edit_user_ssh_keys_add', user_id=user_id),
89 {'description': desc, 'key_data': key_data,
88 {'description': desc, 'key_data': key_data,
90 'csrf_token': self.csrf_token})
89 'csrf_token': self.csrf_token})
91 assert_session_flash(response, 'An error occurred during ssh '
90 assert_session_flash(response, 'An error occurred during ssh '
92 'key saving: Unable to decode the key')
91 'key saving: Unable to decode the key')
93
92
94 def test_ssh_key_duplicate(self, user_util):
93 def test_ssh_key_duplicate(self, user_util):
95 self.log_user()
94 self.log_user()
96 user = user_util.create_user()
95 user = user_util.create_user()
97 user_id = user.user_id
96 user_id = user.user_id
98
97
99 key_data = self.VALID_KEY
98 key_data = self.VALID_KEY
100
99
101 desc = 'MY SSH KEY'
100 desc = 'MY SSH KEY'
102 response = self.app.post(
101 response = self.app.post(
103 route_path('edit_user_ssh_keys_add', user_id=user_id),
102 route_path('edit_user_ssh_keys_add', user_id=user_id),
104 {'description': desc, 'key_data': key_data,
103 {'description': desc, 'key_data': key_data,
105 'csrf_token': self.csrf_token})
104 'csrf_token': self.csrf_token})
106 assert_session_flash(response, 'Ssh Key successfully created')
105 assert_session_flash(response, 'Ssh Key successfully created')
107 response.follow() # flush session flash
106 response.follow() # flush session flash
108
107
109 # add the same key AGAIN
108 # add the same key AGAIN
110 desc = 'MY SSH KEY'
109 desc = 'MY SSH KEY'
111 response = self.app.post(
110 response = self.app.post(
112 route_path('edit_user_ssh_keys_add', user_id=user_id),
111 route_path('edit_user_ssh_keys_add', user_id=user_id),
113 {'description': desc, 'key_data': key_data,
112 {'description': desc, 'key_data': key_data,
114 'csrf_token': self.csrf_token})
113 'csrf_token': self.csrf_token})
115
114
116 err = 'Such key with fingerprint `{}` already exists, ' \
115 err = 'Such key with fingerprint `{}` already exists, ' \
117 'please use a different one'.format(self.FINGERPRINT)
116 'please use a different one'.format(self.FINGERPRINT)
118 assert_session_flash(response, 'An error occurred during ssh key '
117 assert_session_flash(response, 'An error occurred during ssh key '
119 'saving: {}'.format(err))
118 'saving: {}'.format(err))
120
119
121 def test_add_ssh_key(self, user_util):
120 def test_add_ssh_key(self, user_util):
122 self.log_user()
121 self.log_user()
123 user = user_util.create_user()
122 user = user_util.create_user()
124 user_id = user.user_id
123 user_id = user.user_id
125
124
126 key_data = self.VALID_KEY
125 key_data = self.VALID_KEY
127
126
128 desc = 'MY SSH KEY'
127 desc = 'MY SSH KEY'
129 response = self.app.post(
128 response = self.app.post(
130 route_path('edit_user_ssh_keys_add', user_id=user_id),
129 route_path('edit_user_ssh_keys_add', user_id=user_id),
131 {'description': desc, 'key_data': key_data,
130 {'description': desc, 'key_data': key_data,
132 'csrf_token': self.csrf_token})
131 'csrf_token': self.csrf_token})
133 assert_session_flash(response, 'Ssh Key successfully created')
132 assert_session_flash(response, 'Ssh Key successfully created')
134
133
135 response = response.follow()
134 response = response.follow()
136 response.mustcontain(desc)
135 response.mustcontain(desc)
137
136
138 def test_delete_ssh_key(self, user_util):
137 def test_delete_ssh_key(self, user_util):
139 self.log_user()
138 self.log_user()
140 user = user_util.create_user()
139 user = user_util.create_user()
141 user_id = user.user_id
140 user_id = user.user_id
142
141
143 key_data = self.VALID_KEY
142 key_data = self.VALID_KEY
144
143
145 desc = 'MY SSH KEY'
144 desc = 'MY SSH KEY'
146 response = self.app.post(
145 response = self.app.post(
147 route_path('edit_user_ssh_keys_add', user_id=user_id),
146 route_path('edit_user_ssh_keys_add', user_id=user_id),
148 {'description': desc, 'key_data': key_data,
147 {'description': desc, 'key_data': key_data,
149 'csrf_token': self.csrf_token})
148 'csrf_token': self.csrf_token})
150 assert_session_flash(response, 'Ssh Key successfully created')
149 assert_session_flash(response, 'Ssh Key successfully created')
151 response = response.follow() # flush the Session flash
150 response = response.follow() # flush the Session flash
152
151
153 # now delete our key
152 # now delete our key
154 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
153 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
155 assert 1 == len(keys)
154 assert 1 == len(keys)
156
155
157 response = self.app.post(
156 response = self.app.post(
158 route_path('edit_user_ssh_keys_delete', user_id=user_id),
157 route_path('edit_user_ssh_keys_delete', user_id=user_id),
159 {'del_ssh_key': keys[0].ssh_key_id,
158 {'del_ssh_key': keys[0].ssh_key_id,
160 'csrf_token': self.csrf_token})
159 'csrf_token': self.csrf_token})
161
160
162 assert_session_flash(response, 'Ssh key successfully deleted')
161 assert_session_flash(response, 'Ssh key successfully deleted')
163 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
162 keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
164 assert 0 == len(keys)
163 assert 0 == len(keys)
165
164
166 def test_generate_keypair(self, user_util):
165 def test_generate_keypair(self, user_util):
167 self.log_user()
166 self.log_user()
168 user = user_util.create_user()
167 user = user_util.create_user()
169 user_id = user.user_id
168 user_id = user.user_id
170
169
171 response = self.app.get(
170 response = self.app.get(
172 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
171 route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
173
172
174 response.mustcontain('Private key')
173 response.mustcontain('Private key')
175 response.mustcontain('Public key')
174 response.mustcontain('Public key')
176 response.mustcontain('-----BEGIN PRIVATE KEY-----')
175 response.mustcontain('-----BEGIN PRIVATE KEY-----')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,40 +1,40 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from rhodecode.apps._base import BaseAppView, DataGridAppView
23 from rhodecode.apps._base import BaseAppView, DataGridAppView
24 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
24 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
25
25
26 log = logging.getLogger(__name__)
26 log = logging.getLogger(__name__)
27
27
28
28
29 class AdminArtifactsView(BaseAppView, DataGridAppView):
29 class AdminArtifactsView(BaseAppView, DataGridAppView):
30
30
31 def load_default_context(self):
31 def load_default_context(self):
32 c = self._get_local_tmpl_context()
32 c = self._get_local_tmpl_context()
33 return c
33 return c
34
34
35 @LoginRequired()
35 @LoginRequired()
36 @HasPermissionAllDecorator('hg.admin')
36 @HasPermissionAllDecorator('hg.admin')
37 def artifacts(self):
37 def artifacts(self):
38 c = self.load_default_context()
38 c = self.load_default_context()
39 c.active = 'artifacts'
39 c.active = 'artifacts'
40 return self._get_template_context(c)
40 return self._get_template_context(c)
@@ -1,87 +1,87 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPNotFound
23 from pyramid.httpexceptions import HTTPNotFound
24
24
25 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base import BaseAppView
26 from rhodecode.model.db import joinedload, UserLog
26 from rhodecode.model.db import joinedload, UserLog
27 from rhodecode.lib.user_log_filter import user_log_filter
27 from rhodecode.lib.user_log_filter import user_log_filter
28 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
28 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
29 from rhodecode.lib.utils2 import safe_int
29 from rhodecode.lib.utils2 import safe_int
30 from rhodecode.lib.helpers import SqlPage
30 from rhodecode.lib.helpers import SqlPage
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class AdminAuditLogsView(BaseAppView):
35 class AdminAuditLogsView(BaseAppView):
36
36
37 def load_default_context(self):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
39 return c
39 return c
40
40
41 @LoginRequired()
41 @LoginRequired()
42 @HasPermissionAllDecorator('hg.admin')
42 @HasPermissionAllDecorator('hg.admin')
43 def admin_audit_logs(self):
43 def admin_audit_logs(self):
44 c = self.load_default_context()
44 c = self.load_default_context()
45
45
46 users_log = UserLog.query()\
46 users_log = UserLog.query()\
47 .options(joinedload(UserLog.user))\
47 .options(joinedload(UserLog.user))\
48 .options(joinedload(UserLog.repository))
48 .options(joinedload(UserLog.repository))
49
49
50 # FILTERING
50 # FILTERING
51 c.search_term = self.request.GET.get('filter')
51 c.search_term = self.request.GET.get('filter')
52 try:
52 try:
53 users_log = user_log_filter(users_log, c.search_term)
53 users_log = user_log_filter(users_log, c.search_term)
54 except Exception:
54 except Exception:
55 # we want this to crash for now
55 # we want this to crash for now
56 raise
56 raise
57
57
58 users_log = users_log.order_by(UserLog.action_date.desc())
58 users_log = users_log.order_by(UserLog.action_date.desc())
59
59
60 p = safe_int(self.request.GET.get('page', 1), 1)
60 p = safe_int(self.request.GET.get('page', 1), 1)
61
61
62 def url_generator(page_num):
62 def url_generator(page_num):
63 query_params = {
63 query_params = {
64 'page': page_num
64 'page': page_num
65 }
65 }
66 if c.search_term:
66 if c.search_term:
67 query_params['filter'] = c.search_term
67 query_params['filter'] = c.search_term
68 return self.request.current_route_path(_query=query_params)
68 return self.request.current_route_path(_query=query_params)
69
69
70 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
70 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
71 url_maker=url_generator)
71 url_maker=url_generator)
72 return self._get_template_context(c)
72 return self._get_template_context(c)
73
73
74 @LoginRequired()
74 @LoginRequired()
75 @HasPermissionAllDecorator('hg.admin')
75 @HasPermissionAllDecorator('hg.admin')
76 def admin_audit_log_entry(self):
76 def admin_audit_log_entry(self):
77 c = self.load_default_context()
77 c = self.load_default_context()
78 audit_log_id = self.request.matchdict['audit_log_id']
78 audit_log_id = self.request.matchdict['audit_log_id']
79
79
80 c.audit_log_entry = UserLog.query()\
80 c.audit_log_entry = UserLog.query()\
81 .options(joinedload(UserLog.user))\
81 .options(joinedload(UserLog.user))\
82 .options(joinedload(UserLog.repository))\
82 .options(joinedload(UserLog.repository))\
83 .filter(UserLog.user_log_id == audit_log_id).scalar()
83 .filter(UserLog.user_log_id == audit_log_id).scalar()
84 if not c.audit_log_entry:
84 if not c.audit_log_entry:
85 raise HTTPNotFound()
85 raise HTTPNotFound()
86
86
87 return self._get_template_context(c)
87 return self._get_template_context(c)
@@ -1,103 +1,103 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import BaseAppView
30 from rhodecode.apps._base import BaseAppView
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.model.forms import DefaultsForm
34 from rhodecode.model.forms import DefaultsForm
35 from rhodecode.model.meta import Session
35 from rhodecode.model.meta import Session
36 from rhodecode import BACKENDS
36 from rhodecode import BACKENDS
37 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class AdminDefaultSettingsView(BaseAppView):
42 class AdminDefaultSettingsView(BaseAppView):
43
43
44 def load_default_context(self):
44 def load_default_context(self):
45 c = self._get_local_tmpl_context()
45 c = self._get_local_tmpl_context()
46 return c
46 return c
47
47
48 @LoginRequired()
48 @LoginRequired()
49 @HasPermissionAllDecorator('hg.admin')
49 @HasPermissionAllDecorator('hg.admin')
50 def defaults_repository_show(self):
50 def defaults_repository_show(self):
51 c = self.load_default_context()
51 c = self.load_default_context()
52 c.backends = BACKENDS.keys()
52 c.backends = BACKENDS.keys()
53 c.active = 'repositories'
53 c.active = 'repositories'
54 defaults = SettingsModel().get_default_repo_settings()
54 defaults = SettingsModel().get_default_repo_settings()
55
55
56 data = render(
56 data = render(
57 'rhodecode:templates/admin/defaults/defaults.mako',
57 'rhodecode:templates/admin/defaults/defaults.mako',
58 self._get_template_context(c), self.request)
58 self._get_template_context(c), self.request)
59 html = formencode.htmlfill.render(
59 html = formencode.htmlfill.render(
60 data,
60 data,
61 defaults=defaults,
61 defaults=defaults,
62 encoding="UTF-8",
62 encoding="UTF-8",
63 force_defaults=False
63 force_defaults=False
64 )
64 )
65 return Response(html)
65 return Response(html)
66
66
67 @LoginRequired()
67 @LoginRequired()
68 @HasPermissionAllDecorator('hg.admin')
68 @HasPermissionAllDecorator('hg.admin')
69 @CSRFRequired()
69 @CSRFRequired()
70 def defaults_repository_update(self):
70 def defaults_repository_update(self):
71 _ = self.request.translate
71 _ = self.request.translate
72 c = self.load_default_context()
72 c = self.load_default_context()
73 c.active = 'repositories'
73 c.active = 'repositories'
74 form = DefaultsForm(self.request.translate)()
74 form = DefaultsForm(self.request.translate)()
75
75
76 try:
76 try:
77 form_result = form.to_python(dict(self.request.POST))
77 form_result = form.to_python(dict(self.request.POST))
78 for k, v in form_result.items():
78 for k, v in form_result.items():
79 setting = SettingsModel().create_or_update_setting(k, v)
79 setting = SettingsModel().create_or_update_setting(k, v)
80 Session().add(setting)
80 Session().add(setting)
81 Session().commit()
81 Session().commit()
82 h.flash(_('Default settings updated successfully'),
82 h.flash(_('Default settings updated successfully'),
83 category='success')
83 category='success')
84
84
85 except formencode.Invalid as errors:
85 except formencode.Invalid as errors:
86 data = render(
86 data = render(
87 'rhodecode:templates/admin/defaults/defaults.mako',
87 'rhodecode:templates/admin/defaults/defaults.mako',
88 self._get_template_context(c), self.request)
88 self._get_template_context(c), self.request)
89 html = formencode.htmlfill.render(
89 html = formencode.htmlfill.render(
90 data,
90 data,
91 defaults=errors.value,
91 defaults=errors.value,
92 errors=errors.unpack_errors() or {},
92 errors=errors.unpack_errors() or {},
93 prefix_error=False,
93 prefix_error=False,
94 encoding="UTF-8",
94 encoding="UTF-8",
95 force_defaults=False
95 force_defaults=False
96 )
96 )
97 return Response(html)
97 return Response(html)
98 except Exception:
98 except Exception:
99 log.exception('Exception in update action')
99 log.exception('Exception in update action')
100 h.flash(_('Error occurred during update of default values'),
100 h.flash(_('Error occurred during update of default values'),
101 category='error')
101 category='error')
102
102
103 raise HTTPFound(h.route_path('admin_defaults_repositories'))
103 raise HTTPFound(h.route_path('admin_defaults_repositories'))
@@ -1,161 +1,161 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2018-2020 RhodeCode GmbH
3 # Copyright (C) 2018-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
20 import os
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPFound
23 from pyramid.httpexceptions import HTTPFound
24
24
25 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base.navigation import navigation_list
26 from rhodecode.apps._base.navigation import navigation_list
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import (
28 from rhodecode.lib.auth import (
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
30 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
31 from rhodecode.lib import exc_tracking
31 from rhodecode.lib import exc_tracking
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class ExceptionsTrackerView(BaseAppView):
36 class ExceptionsTrackerView(BaseAppView):
37 def load_default_context(self):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
39 c.navlist = navigation_list(self.request)
39 c.navlist = navigation_list(self.request)
40 return c
40 return c
41
41
42 def count_all_exceptions(self):
42 def count_all_exceptions(self):
43 exc_store_path = exc_tracking.get_exc_store()
43 exc_store_path = exc_tracking.get_exc_store()
44 count = 0
44 count = 0
45 for fname in os.listdir(exc_store_path):
45 for fname in os.listdir(exc_store_path):
46 parts = fname.split('_', 2)
46 parts = fname.split('_', 2)
47 if not len(parts) == 3:
47 if not len(parts) == 3:
48 continue
48 continue
49 count +=1
49 count +=1
50 return count
50 return count
51
51
52 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
52 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
53 exc_store_path = exc_tracking.get_exc_store()
53 exc_store_path = exc_tracking.get_exc_store()
54 exception_list = []
54 exception_list = []
55
55
56 def key_sorter(val):
56 def key_sorter(val):
57 try:
57 try:
58 return val.split('_')[-1]
58 return val.split('_')[-1]
59 except Exception:
59 except Exception:
60 return 0
60 return 0
61
61
62 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
62 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
63
63
64 parts = fname.split('_', 2)
64 parts = fname.split('_', 2)
65 if not len(parts) == 3:
65 if not len(parts) == 3:
66 continue
66 continue
67
67
68 exc_id, app_type, exc_timestamp = parts
68 exc_id, app_type, exc_timestamp = parts
69
69
70 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
70 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
71 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
71 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
72
72
73 if read_metadata:
73 if read_metadata:
74 full_path = os.path.join(exc_store_path, fname)
74 full_path = os.path.join(exc_store_path, fname)
75 if not os.path.isfile(full_path):
75 if not os.path.isfile(full_path):
76 continue
76 continue
77 try:
77 try:
78 # we can read our metadata
78 # we can read our metadata
79 with open(full_path, 'rb') as f:
79 with open(full_path, 'rb') as f:
80 exc_metadata = exc_tracking.exc_unserialize(f.read())
80 exc_metadata = exc_tracking.exc_unserialize(f.read())
81 exc.update(exc_metadata)
81 exc.update(exc_metadata)
82 except Exception:
82 except Exception:
83 log.exception('Failed to read exc data from:{}'.format(full_path))
83 log.exception('Failed to read exc data from:{}'.format(full_path))
84 pass
84 pass
85 # convert our timestamp to a date obj, for nicer representation
85 # convert our timestamp to a date obj, for nicer representation
86 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
86 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
87
87
88 type_present = exc.get('exc_type')
88 type_present = exc.get('exc_type')
89 if type_filter:
89 if type_filter:
90 if type_present and type_present == type_filter:
90 if type_present and type_present == type_filter:
91 exception_list.append(exc)
91 exception_list.append(exc)
92 else:
92 else:
93 exception_list.append(exc)
93 exception_list.append(exc)
94
94
95 if limit and len(exception_list) >= limit:
95 if limit and len(exception_list) >= limit:
96 break
96 break
97 return exception_list
97 return exception_list
98
98
99 @LoginRequired()
99 @LoginRequired()
100 @HasPermissionAllDecorator('hg.admin')
100 @HasPermissionAllDecorator('hg.admin')
101 def browse_exceptions(self):
101 def browse_exceptions(self):
102 _ = self.request.translate
102 _ = self.request.translate
103 c = self.load_default_context()
103 c = self.load_default_context()
104 c.active = 'exceptions_browse'
104 c.active = 'exceptions_browse'
105 c.limit = safe_int(self.request.GET.get('limit')) or 50
105 c.limit = safe_int(self.request.GET.get('limit')) or 50
106 c.type_filter = self.request.GET.get('type_filter')
106 c.type_filter = self.request.GET.get('type_filter')
107 c.next_limit = c.limit + 50
107 c.next_limit = c.limit + 50
108 c.exception_list = self.get_all_exceptions(
108 c.exception_list = self.get_all_exceptions(
109 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
109 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
110 c.exception_list_count = self.count_all_exceptions()
110 c.exception_list_count = self.count_all_exceptions()
111 c.exception_store_dir = exc_tracking.get_exc_store()
111 c.exception_store_dir = exc_tracking.get_exc_store()
112 return self._get_template_context(c)
112 return self._get_template_context(c)
113
113
114 @LoginRequired()
114 @LoginRequired()
115 @HasPermissionAllDecorator('hg.admin')
115 @HasPermissionAllDecorator('hg.admin')
116 def exception_show(self):
116 def exception_show(self):
117 _ = self.request.translate
117 _ = self.request.translate
118 c = self.load_default_context()
118 c = self.load_default_context()
119
119
120 c.active = 'exceptions'
120 c.active = 'exceptions'
121 c.exception_id = self.request.matchdict['exception_id']
121 c.exception_id = self.request.matchdict['exception_id']
122 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
122 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
123 return self._get_template_context(c)
123 return self._get_template_context(c)
124
124
125 @LoginRequired()
125 @LoginRequired()
126 @HasPermissionAllDecorator('hg.admin')
126 @HasPermissionAllDecorator('hg.admin')
127 @CSRFRequired()
127 @CSRFRequired()
128 def exception_delete_all(self):
128 def exception_delete_all(self):
129 _ = self.request.translate
129 _ = self.request.translate
130 c = self.load_default_context()
130 c = self.load_default_context()
131 type_filter = self.request.POST.get('type_filter')
131 type_filter = self.request.POST.get('type_filter')
132
132
133 c.active = 'exceptions'
133 c.active = 'exceptions'
134 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
134 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
135 exc_count = 0
135 exc_count = 0
136
136
137 for exc in all_exc:
137 for exc in all_exc:
138 if type_filter:
138 if type_filter:
139 if exc.get('exc_type') == type_filter:
139 if exc.get('exc_type') == type_filter:
140 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
140 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
141 exc_count += 1
141 exc_count += 1
142 else:
142 else:
143 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
143 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
144 exc_count += 1
144 exc_count += 1
145
145
146 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
146 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
147 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
147 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
148
148
149 @LoginRequired()
149 @LoginRequired()
150 @HasPermissionAllDecorator('hg.admin')
150 @HasPermissionAllDecorator('hg.admin')
151 @CSRFRequired()
151 @CSRFRequired()
152 def exception_delete(self):
152 def exception_delete(self):
153 _ = self.request.translate
153 _ = self.request.translate
154 c = self.load_default_context()
154 c = self.load_default_context()
155
155
156 c.active = 'exceptions'
156 c.active = 'exceptions'
157 c.exception_id = self.request.matchdict['exception_id']
157 c.exception_id = self.request.matchdict['exception_id']
158 exc_tracking.delete_exception(c.exception_id, prefix=None)
158 exc_tracking.delete_exception(c.exception_id, prefix=None)
159
159
160 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
160 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
161 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
161 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
@@ -1,72 +1,72 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24
24
25 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base import BaseAppView
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
27 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
28 from rhodecode.model.db import PullRequest
28 from rhodecode.model.db import PullRequest
29
29
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class AdminMainView(BaseAppView):
34 class AdminMainView(BaseAppView):
35 def load_default_context(self):
35 def load_default_context(self):
36 c = self._get_local_tmpl_context()
36 c = self._get_local_tmpl_context()
37 return c
37 return c
38
38
39 @LoginRequired()
39 @LoginRequired()
40 @NotAnonymous()
40 @NotAnonymous()
41 def admin_main(self):
41 def admin_main(self):
42 c = self.load_default_context()
42 c = self.load_default_context()
43 c.active = 'admin'
43 c.active = 'admin'
44
44
45 if not (c.is_super_admin or c.is_delegated_admin):
45 if not (c.is_super_admin or c.is_delegated_admin):
46 raise HTTPNotFound()
46 raise HTTPNotFound()
47
47
48 return self._get_template_context(c)
48 return self._get_template_context(c)
49
49
50 @LoginRequired()
50 @LoginRequired()
51 def pull_requests(self):
51 def pull_requests(self):
52 """
52 """
53 Global redirect for Pull Requests
53 Global redirect for Pull Requests
54 pull_request_id: id of pull requests in the system
54 pull_request_id: id of pull requests in the system
55 """
55 """
56
56
57 pull_request = PullRequest.get_or_404(
57 pull_request = PullRequest.get_or_404(
58 self.request.matchdict['pull_request_id'])
58 self.request.matchdict['pull_request_id'])
59 pull_request_id = pull_request.pull_request_id
59 pull_request_id = pull_request.pull_request_id
60
60
61 repo_name = pull_request.target_repo.repo_name
61 repo_name = pull_request.target_repo.repo_name
62 # NOTE(marcink):
62 # NOTE(marcink):
63 # check permissions so we don't redirect to repo that we don't have access to
63 # check permissions so we don't redirect to repo that we don't have access to
64 # exposing it's name
64 # exposing it's name
65 target_repo_perm = HasRepoPermissionAny(
65 target_repo_perm = HasRepoPermissionAny(
66 'repository.read', 'repository.write', 'repository.admin')(repo_name)
66 'repository.read', 'repository.write', 'repository.admin')(repo_name)
67 if not target_repo_perm:
67 if not target_repo_perm:
68 raise HTTPNotFound()
68 raise HTTPNotFound()
69
69
70 raise HTTPFound(
70 raise HTTPFound(
71 h.route_path('pullrequest_show', repo_name=repo_name,
71 h.route_path('pullrequest_show', repo_name=repo_name,
72 pull_request_id=pull_request_id))
72 pull_request_id=pull_request_id))
@@ -1,46 +1,46 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import collections
21 import collections
22 import logging
22 import logging
23
23
24 from rhodecode.apps._base import BaseAppView
24 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base.navigation import navigation_list
25 from rhodecode.apps._base.navigation import navigation_list
26 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
26 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
27 from rhodecode.lib.utils import read_opensource_licenses
27 from rhodecode.lib.utils import read_opensource_licenses
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class OpenSourceLicensesAdminSettingsView(BaseAppView):
32 class OpenSourceLicensesAdminSettingsView(BaseAppView):
33
33
34 def load_default_context(self):
34 def load_default_context(self):
35 c = self._get_local_tmpl_context()
35 c = self._get_local_tmpl_context()
36 return c
36 return c
37
37
38 @LoginRequired()
38 @LoginRequired()
39 @HasPermissionAllDecorator('hg.admin')
39 @HasPermissionAllDecorator('hg.admin')
40 def open_source_licenses(self):
40 def open_source_licenses(self):
41 c = self.load_default_context()
41 c = self.load_default_context()
42 c.active = 'open_source'
42 c.active = 'open_source'
43 c.navlist = navigation_list(self.request)
43 c.navlist = navigation_list(self.request)
44 c.opensource_licenses = sorted(
44 c.opensource_licenses = sorted(
45 read_opensource_licenses(), key=lambda d: d["name"])
45 read_opensource_licenses(), key=lambda d: d["name"])
46 return self._get_template_context(c)
46 return self._get_template_context(c)
@@ -1,479 +1,479 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import re
21 import re
22 import logging
22 import logging
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25 import datetime
25 import datetime
26 from pyramid.interfaces import IRoutesMapper
26 from pyramid.interfaces import IRoutesMapper
27
27
28 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 from rhodecode import events
34 from rhodecode import events
35
35
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 from rhodecode.lib.utils2 import aslist, safe_unicode
39 from rhodecode.lib.utils2 import aslist, safe_unicode
40 from rhodecode.model.db import (
40 from rhodecode.model.db import (
41 or_, coalesce, User, UserIpMap, UserSshKeys)
41 or_, coalesce, User, UserIpMap, UserSshKeys)
42 from rhodecode.model.forms import (
42 from rhodecode.model.forms import (
43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.permission import PermissionModel
45 from rhodecode.model.permission import PermissionModel
46 from rhodecode.model.settings import SettingsModel
46 from rhodecode.model.settings import SettingsModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51
51
52 class AdminPermissionsView(BaseAppView, DataGridAppView):
52 class AdminPermissionsView(BaseAppView, DataGridAppView):
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55 PermissionModel().set_global_permission_choices(
55 PermissionModel().set_global_permission_choices(
56 c, gettext_translator=self.request.translate)
56 c, gettext_translator=self.request.translate)
57 return c
57 return c
58
58
59 @LoginRequired()
59 @LoginRequired()
60 @HasPermissionAllDecorator('hg.admin')
60 @HasPermissionAllDecorator('hg.admin')
61 def permissions_application(self):
61 def permissions_application(self):
62 c = self.load_default_context()
62 c = self.load_default_context()
63 c.active = 'application'
63 c.active = 'application'
64
64
65 c.user = User.get_default_user(refresh=True)
65 c.user = User.get_default_user(refresh=True)
66
66
67 app_settings = c.rc_config
67 app_settings = c.rc_config
68
68
69 defaults = {
69 defaults = {
70 'anonymous': c.user.active,
70 'anonymous': c.user.active,
71 'default_register_message': app_settings.get(
71 'default_register_message': app_settings.get(
72 'rhodecode_register_message')
72 'rhodecode_register_message')
73 }
73 }
74 defaults.update(c.user.get_default_perms())
74 defaults.update(c.user.get_default_perms())
75
75
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
77 self._get_template_context(c), self.request)
77 self._get_template_context(c), self.request)
78 html = formencode.htmlfill.render(
78 html = formencode.htmlfill.render(
79 data,
79 data,
80 defaults=defaults,
80 defaults=defaults,
81 encoding="UTF-8",
81 encoding="UTF-8",
82 force_defaults=False
82 force_defaults=False
83 )
83 )
84 return Response(html)
84 return Response(html)
85
85
86 @LoginRequired()
86 @LoginRequired()
87 @HasPermissionAllDecorator('hg.admin')
87 @HasPermissionAllDecorator('hg.admin')
88 @CSRFRequired()
88 @CSRFRequired()
89 def permissions_application_update(self):
89 def permissions_application_update(self):
90 _ = self.request.translate
90 _ = self.request.translate
91 c = self.load_default_context()
91 c = self.load_default_context()
92 c.active = 'application'
92 c.active = 'application'
93
93
94 _form = ApplicationPermissionsForm(
94 _form = ApplicationPermissionsForm(
95 self.request.translate,
95 self.request.translate,
96 [x[0] for x in c.register_choices],
96 [x[0] for x in c.register_choices],
97 [x[0] for x in c.password_reset_choices],
97 [x[0] for x in c.password_reset_choices],
98 [x[0] for x in c.extern_activate_choices])()
98 [x[0] for x in c.extern_activate_choices])()
99
99
100 try:
100 try:
101 form_result = _form.to_python(dict(self.request.POST))
101 form_result = _form.to_python(dict(self.request.POST))
102 form_result.update({'perm_user_name': User.DEFAULT_USER})
102 form_result.update({'perm_user_name': User.DEFAULT_USER})
103 PermissionModel().update_application_permissions(form_result)
103 PermissionModel().update_application_permissions(form_result)
104
104
105 settings = [
105 settings = [
106 ('register_message', 'default_register_message'),
106 ('register_message', 'default_register_message'),
107 ]
107 ]
108 for setting, form_key in settings:
108 for setting, form_key in settings:
109 sett = SettingsModel().create_or_update_setting(
109 sett = SettingsModel().create_or_update_setting(
110 setting, form_result[form_key])
110 setting, form_result[form_key])
111 Session().add(sett)
111 Session().add(sett)
112
112
113 Session().commit()
113 Session().commit()
114 h.flash(_('Application permissions updated successfully'),
114 h.flash(_('Application permissions updated successfully'),
115 category='success')
115 category='success')
116
116
117 except formencode.Invalid as errors:
117 except formencode.Invalid as errors:
118 defaults = errors.value
118 defaults = errors.value
119
119
120 data = render(
120 data = render(
121 'rhodecode:templates/admin/permissions/permissions.mako',
121 'rhodecode:templates/admin/permissions/permissions.mako',
122 self._get_template_context(c), self.request)
122 self._get_template_context(c), self.request)
123 html = formencode.htmlfill.render(
123 html = formencode.htmlfill.render(
124 data,
124 data,
125 defaults=defaults,
125 defaults=defaults,
126 errors=errors.unpack_errors() or {},
126 errors=errors.unpack_errors() or {},
127 prefix_error=False,
127 prefix_error=False,
128 encoding="UTF-8",
128 encoding="UTF-8",
129 force_defaults=False
129 force_defaults=False
130 )
130 )
131 return Response(html)
131 return Response(html)
132
132
133 except Exception:
133 except Exception:
134 log.exception("Exception during update of permissions")
134 log.exception("Exception during update of permissions")
135 h.flash(_('Error occurred during update of permissions'),
135 h.flash(_('Error occurred during update of permissions'),
136 category='error')
136 category='error')
137
137
138 affected_user_ids = [User.get_default_user_id()]
138 affected_user_ids = [User.get_default_user_id()]
139 PermissionModel().trigger_permission_flush(affected_user_ids)
139 PermissionModel().trigger_permission_flush(affected_user_ids)
140
140
141 raise HTTPFound(h.route_path('admin_permissions_application'))
141 raise HTTPFound(h.route_path('admin_permissions_application'))
142
142
143 @LoginRequired()
143 @LoginRequired()
144 @HasPermissionAllDecorator('hg.admin')
144 @HasPermissionAllDecorator('hg.admin')
145 def permissions_objects(self):
145 def permissions_objects(self):
146 c = self.load_default_context()
146 c = self.load_default_context()
147 c.active = 'objects'
147 c.active = 'objects'
148
148
149 c.user = User.get_default_user(refresh=True)
149 c.user = User.get_default_user(refresh=True)
150 defaults = {}
150 defaults = {}
151 defaults.update(c.user.get_default_perms())
151 defaults.update(c.user.get_default_perms())
152
152
153 data = render(
153 data = render(
154 'rhodecode:templates/admin/permissions/permissions.mako',
154 'rhodecode:templates/admin/permissions/permissions.mako',
155 self._get_template_context(c), self.request)
155 self._get_template_context(c), self.request)
156 html = formencode.htmlfill.render(
156 html = formencode.htmlfill.render(
157 data,
157 data,
158 defaults=defaults,
158 defaults=defaults,
159 encoding="UTF-8",
159 encoding="UTF-8",
160 force_defaults=False
160 force_defaults=False
161 )
161 )
162 return Response(html)
162 return Response(html)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @HasPermissionAllDecorator('hg.admin')
165 @HasPermissionAllDecorator('hg.admin')
166 @CSRFRequired()
166 @CSRFRequired()
167 def permissions_objects_update(self):
167 def permissions_objects_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'objects'
170 c.active = 'objects'
171
171
172 _form = ObjectPermissionsForm(
172 _form = ObjectPermissionsForm(
173 self.request.translate,
173 self.request.translate,
174 [x[0] for x in c.repo_perms_choices],
174 [x[0] for x in c.repo_perms_choices],
175 [x[0] for x in c.group_perms_choices],
175 [x[0] for x in c.group_perms_choices],
176 [x[0] for x in c.user_group_perms_choices],
176 [x[0] for x in c.user_group_perms_choices],
177 )()
177 )()
178
178
179 try:
179 try:
180 form_result = _form.to_python(dict(self.request.POST))
180 form_result = _form.to_python(dict(self.request.POST))
181 form_result.update({'perm_user_name': User.DEFAULT_USER})
181 form_result.update({'perm_user_name': User.DEFAULT_USER})
182 PermissionModel().update_object_permissions(form_result)
182 PermissionModel().update_object_permissions(form_result)
183
183
184 Session().commit()
184 Session().commit()
185 h.flash(_('Object permissions updated successfully'),
185 h.flash(_('Object permissions updated successfully'),
186 category='success')
186 category='success')
187
187
188 except formencode.Invalid as errors:
188 except formencode.Invalid as errors:
189 defaults = errors.value
189 defaults = errors.value
190
190
191 data = render(
191 data = render(
192 'rhodecode:templates/admin/permissions/permissions.mako',
192 'rhodecode:templates/admin/permissions/permissions.mako',
193 self._get_template_context(c), self.request)
193 self._get_template_context(c), self.request)
194 html = formencode.htmlfill.render(
194 html = formencode.htmlfill.render(
195 data,
195 data,
196 defaults=defaults,
196 defaults=defaults,
197 errors=errors.unpack_errors() or {},
197 errors=errors.unpack_errors() or {},
198 prefix_error=False,
198 prefix_error=False,
199 encoding="UTF-8",
199 encoding="UTF-8",
200 force_defaults=False
200 force_defaults=False
201 )
201 )
202 return Response(html)
202 return Response(html)
203 except Exception:
203 except Exception:
204 log.exception("Exception during update of permissions")
204 log.exception("Exception during update of permissions")
205 h.flash(_('Error occurred during update of permissions'),
205 h.flash(_('Error occurred during update of permissions'),
206 category='error')
206 category='error')
207
207
208 affected_user_ids = [User.get_default_user_id()]
208 affected_user_ids = [User.get_default_user_id()]
209 PermissionModel().trigger_permission_flush(affected_user_ids)
209 PermissionModel().trigger_permission_flush(affected_user_ids)
210
210
211 raise HTTPFound(h.route_path('admin_permissions_object'))
211 raise HTTPFound(h.route_path('admin_permissions_object'))
212
212
213 @LoginRequired()
213 @LoginRequired()
214 @HasPermissionAllDecorator('hg.admin')
214 @HasPermissionAllDecorator('hg.admin')
215 def permissions_branch(self):
215 def permissions_branch(self):
216 c = self.load_default_context()
216 c = self.load_default_context()
217 c.active = 'branch'
217 c.active = 'branch'
218
218
219 c.user = User.get_default_user(refresh=True)
219 c.user = User.get_default_user(refresh=True)
220 defaults = {}
220 defaults = {}
221 defaults.update(c.user.get_default_perms())
221 defaults.update(c.user.get_default_perms())
222
222
223 data = render(
223 data = render(
224 'rhodecode:templates/admin/permissions/permissions.mako',
224 'rhodecode:templates/admin/permissions/permissions.mako',
225 self._get_template_context(c), self.request)
225 self._get_template_context(c), self.request)
226 html = formencode.htmlfill.render(
226 html = formencode.htmlfill.render(
227 data,
227 data,
228 defaults=defaults,
228 defaults=defaults,
229 encoding="UTF-8",
229 encoding="UTF-8",
230 force_defaults=False
230 force_defaults=False
231 )
231 )
232 return Response(html)
232 return Response(html)
233
233
234 @LoginRequired()
234 @LoginRequired()
235 @HasPermissionAllDecorator('hg.admin')
235 @HasPermissionAllDecorator('hg.admin')
236 def permissions_global(self):
236 def permissions_global(self):
237 c = self.load_default_context()
237 c = self.load_default_context()
238 c.active = 'global'
238 c.active = 'global'
239
239
240 c.user = User.get_default_user(refresh=True)
240 c.user = User.get_default_user(refresh=True)
241 defaults = {}
241 defaults = {}
242 defaults.update(c.user.get_default_perms())
242 defaults.update(c.user.get_default_perms())
243
243
244 data = render(
244 data = render(
245 'rhodecode:templates/admin/permissions/permissions.mako',
245 'rhodecode:templates/admin/permissions/permissions.mako',
246 self._get_template_context(c), self.request)
246 self._get_template_context(c), self.request)
247 html = formencode.htmlfill.render(
247 html = formencode.htmlfill.render(
248 data,
248 data,
249 defaults=defaults,
249 defaults=defaults,
250 encoding="UTF-8",
250 encoding="UTF-8",
251 force_defaults=False
251 force_defaults=False
252 )
252 )
253 return Response(html)
253 return Response(html)
254
254
255 @LoginRequired()
255 @LoginRequired()
256 @HasPermissionAllDecorator('hg.admin')
256 @HasPermissionAllDecorator('hg.admin')
257 @CSRFRequired()
257 @CSRFRequired()
258 def permissions_global_update(self):
258 def permissions_global_update(self):
259 _ = self.request.translate
259 _ = self.request.translate
260 c = self.load_default_context()
260 c = self.load_default_context()
261 c.active = 'global'
261 c.active = 'global'
262
262
263 _form = UserPermissionsForm(
263 _form = UserPermissionsForm(
264 self.request.translate,
264 self.request.translate,
265 [x[0] for x in c.repo_create_choices],
265 [x[0] for x in c.repo_create_choices],
266 [x[0] for x in c.repo_create_on_write_choices],
266 [x[0] for x in c.repo_create_on_write_choices],
267 [x[0] for x in c.repo_group_create_choices],
267 [x[0] for x in c.repo_group_create_choices],
268 [x[0] for x in c.user_group_create_choices],
268 [x[0] for x in c.user_group_create_choices],
269 [x[0] for x in c.fork_choices],
269 [x[0] for x in c.fork_choices],
270 [x[0] for x in c.inherit_default_permission_choices])()
270 [x[0] for x in c.inherit_default_permission_choices])()
271
271
272 try:
272 try:
273 form_result = _form.to_python(dict(self.request.POST))
273 form_result = _form.to_python(dict(self.request.POST))
274 form_result.update({'perm_user_name': User.DEFAULT_USER})
274 form_result.update({'perm_user_name': User.DEFAULT_USER})
275 PermissionModel().update_user_permissions(form_result)
275 PermissionModel().update_user_permissions(form_result)
276
276
277 Session().commit()
277 Session().commit()
278 h.flash(_('Global permissions updated successfully'),
278 h.flash(_('Global permissions updated successfully'),
279 category='success')
279 category='success')
280
280
281 except formencode.Invalid as errors:
281 except formencode.Invalid as errors:
282 defaults = errors.value
282 defaults = errors.value
283
283
284 data = render(
284 data = render(
285 'rhodecode:templates/admin/permissions/permissions.mako',
285 'rhodecode:templates/admin/permissions/permissions.mako',
286 self._get_template_context(c), self.request)
286 self._get_template_context(c), self.request)
287 html = formencode.htmlfill.render(
287 html = formencode.htmlfill.render(
288 data,
288 data,
289 defaults=defaults,
289 defaults=defaults,
290 errors=errors.unpack_errors() or {},
290 errors=errors.unpack_errors() or {},
291 prefix_error=False,
291 prefix_error=False,
292 encoding="UTF-8",
292 encoding="UTF-8",
293 force_defaults=False
293 force_defaults=False
294 )
294 )
295 return Response(html)
295 return Response(html)
296 except Exception:
296 except Exception:
297 log.exception("Exception during update of permissions")
297 log.exception("Exception during update of permissions")
298 h.flash(_('Error occurred during update of permissions'),
298 h.flash(_('Error occurred during update of permissions'),
299 category='error')
299 category='error')
300
300
301 affected_user_ids = [User.get_default_user_id()]
301 affected_user_ids = [User.get_default_user_id()]
302 PermissionModel().trigger_permission_flush(affected_user_ids)
302 PermissionModel().trigger_permission_flush(affected_user_ids)
303
303
304 raise HTTPFound(h.route_path('admin_permissions_global'))
304 raise HTTPFound(h.route_path('admin_permissions_global'))
305
305
306 @LoginRequired()
306 @LoginRequired()
307 @HasPermissionAllDecorator('hg.admin')
307 @HasPermissionAllDecorator('hg.admin')
308 def permissions_ips(self):
308 def permissions_ips(self):
309 c = self.load_default_context()
309 c = self.load_default_context()
310 c.active = 'ips'
310 c.active = 'ips'
311
311
312 c.user = User.get_default_user(refresh=True)
312 c.user = User.get_default_user(refresh=True)
313 c.user_ip_map = (
313 c.user_ip_map = (
314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
315
315
316 return self._get_template_context(c)
316 return self._get_template_context(c)
317
317
318 @LoginRequired()
318 @LoginRequired()
319 @HasPermissionAllDecorator('hg.admin')
319 @HasPermissionAllDecorator('hg.admin')
320 def permissions_overview(self):
320 def permissions_overview(self):
321 c = self.load_default_context()
321 c = self.load_default_context()
322 c.active = 'perms'
322 c.active = 'perms'
323
323
324 c.user = User.get_default_user(refresh=True)
324 c.user = User.get_default_user(refresh=True)
325 c.perm_user = c.user.AuthUser()
325 c.perm_user = c.user.AuthUser()
326 return self._get_template_context(c)
326 return self._get_template_context(c)
327
327
328 @LoginRequired()
328 @LoginRequired()
329 @HasPermissionAllDecorator('hg.admin')
329 @HasPermissionAllDecorator('hg.admin')
330 def auth_token_access(self):
330 def auth_token_access(self):
331 from rhodecode import CONFIG
331 from rhodecode import CONFIG
332
332
333 c = self.load_default_context()
333 c = self.load_default_context()
334 c.active = 'auth_token_access'
334 c.active = 'auth_token_access'
335
335
336 c.user = User.get_default_user(refresh=True)
336 c.user = User.get_default_user(refresh=True)
337 c.perm_user = c.user.AuthUser()
337 c.perm_user = c.user.AuthUser()
338
338
339 mapper = self.request.registry.queryUtility(IRoutesMapper)
339 mapper = self.request.registry.queryUtility(IRoutesMapper)
340 c.view_data = []
340 c.view_data = []
341
341
342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
342 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
343 introspector = self.request.registry.introspector
343 introspector = self.request.registry.introspector
344
344
345 view_intr = {}
345 view_intr = {}
346 for view_data in introspector.get_category('views'):
346 for view_data in introspector.get_category('views'):
347 intr = view_data['introspectable']
347 intr = view_data['introspectable']
348
348
349 if 'route_name' in intr and intr['attr']:
349 if 'route_name' in intr and intr['attr']:
350 view_intr[intr['route_name']] = '{}:{}'.format(
350 view_intr[intr['route_name']] = '{}:{}'.format(
351 str(intr['derived_callable'].__name__), intr['attr']
351 str(intr['derived_callable'].__name__), intr['attr']
352 )
352 )
353
353
354 c.whitelist_key = 'api_access_controllers_whitelist'
354 c.whitelist_key = 'api_access_controllers_whitelist'
355 c.whitelist_file = CONFIG.get('__file__')
355 c.whitelist_file = CONFIG.get('__file__')
356 whitelist_views = aslist(
356 whitelist_views = aslist(
357 CONFIG.get(c.whitelist_key), sep=',')
357 CONFIG.get(c.whitelist_key), sep=',')
358
358
359 for route_info in mapper.get_routes():
359 for route_info in mapper.get_routes():
360 if not route_info.name.startswith('__'):
360 if not route_info.name.startswith('__'):
361 routepath = route_info.pattern
361 routepath = route_info.pattern
362
362
363 def replace(matchobj):
363 def replace(matchobj):
364 if matchobj.group(1):
364 if matchobj.group(1):
365 return "{%s}" % matchobj.group(1).split(':')[0]
365 return "{%s}" % matchobj.group(1).split(':')[0]
366 else:
366 else:
367 return "{%s}" % matchobj.group(2)
367 return "{%s}" % matchobj.group(2)
368
368
369 routepath = _argument_prog.sub(replace, routepath)
369 routepath = _argument_prog.sub(replace, routepath)
370
370
371 if not routepath.startswith('/'):
371 if not routepath.startswith('/'):
372 routepath = '/' + routepath
372 routepath = '/' + routepath
373
373
374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
375 active = view_fqn in whitelist_views
375 active = view_fqn in whitelist_views
376 c.view_data.append((route_info.name, view_fqn, routepath, active))
376 c.view_data.append((route_info.name, view_fqn, routepath, active))
377
377
378 c.whitelist_views = whitelist_views
378 c.whitelist_views = whitelist_views
379 return self._get_template_context(c)
379 return self._get_template_context(c)
380
380
381 def ssh_enabled(self):
381 def ssh_enabled(self):
382 return self.request.registry.settings.get(
382 return self.request.registry.settings.get(
383 'ssh.generate_authorized_keyfile')
383 'ssh.generate_authorized_keyfile')
384
384
385 @LoginRequired()
385 @LoginRequired()
386 @HasPermissionAllDecorator('hg.admin')
386 @HasPermissionAllDecorator('hg.admin')
387 def ssh_keys(self):
387 def ssh_keys(self):
388 c = self.load_default_context()
388 c = self.load_default_context()
389 c.active = 'ssh_keys'
389 c.active = 'ssh_keys'
390 c.ssh_enabled = self.ssh_enabled()
390 c.ssh_enabled = self.ssh_enabled()
391 return self._get_template_context(c)
391 return self._get_template_context(c)
392
392
393 @LoginRequired()
393 @LoginRequired()
394 @HasPermissionAllDecorator('hg.admin')
394 @HasPermissionAllDecorator('hg.admin')
395 def ssh_keys_data(self):
395 def ssh_keys_data(self):
396 _ = self.request.translate
396 _ = self.request.translate
397 self.load_default_context()
397 self.load_default_context()
398 column_map = {
398 column_map = {
399 'fingerprint': 'ssh_key_fingerprint',
399 'fingerprint': 'ssh_key_fingerprint',
400 'username': User.username
400 'username': User.username
401 }
401 }
402 draw, start, limit = self._extract_chunk(self.request)
402 draw, start, limit = self._extract_chunk(self.request)
403 search_q, order_by, order_dir = self._extract_ordering(
403 search_q, order_by, order_dir = self._extract_ordering(
404 self.request, column_map=column_map)
404 self.request, column_map=column_map)
405
405
406 ssh_keys_data_total_count = UserSshKeys.query()\
406 ssh_keys_data_total_count = UserSshKeys.query()\
407 .count()
407 .count()
408
408
409 # json generate
409 # json generate
410 base_q = UserSshKeys.query().join(UserSshKeys.user)
410 base_q = UserSshKeys.query().join(UserSshKeys.user)
411
411
412 if search_q:
412 if search_q:
413 like_expression = u'%{}%'.format(safe_unicode(search_q))
413 like_expression = u'%{}%'.format(safe_unicode(search_q))
414 base_q = base_q.filter(or_(
414 base_q = base_q.filter(or_(
415 User.username.ilike(like_expression),
415 User.username.ilike(like_expression),
416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
417 ))
417 ))
418
418
419 users_data_total_filtered_count = base_q.count()
419 users_data_total_filtered_count = base_q.count()
420
420
421 sort_col = self._get_order_col(order_by, UserSshKeys)
421 sort_col = self._get_order_col(order_by, UserSshKeys)
422 if sort_col:
422 if sort_col:
423 if order_dir == 'asc':
423 if order_dir == 'asc':
424 # handle null values properly to order by NULL last
424 # handle null values properly to order by NULL last
425 if order_by in ['created_on']:
425 if order_by in ['created_on']:
426 sort_col = coalesce(sort_col, datetime.date.max)
426 sort_col = coalesce(sort_col, datetime.date.max)
427 sort_col = sort_col.asc()
427 sort_col = sort_col.asc()
428 else:
428 else:
429 # handle null values properly to order by NULL last
429 # handle null values properly to order by NULL last
430 if order_by in ['created_on']:
430 if order_by in ['created_on']:
431 sort_col = coalesce(sort_col, datetime.date.min)
431 sort_col = coalesce(sort_col, datetime.date.min)
432 sort_col = sort_col.desc()
432 sort_col = sort_col.desc()
433
433
434 base_q = base_q.order_by(sort_col)
434 base_q = base_q.order_by(sort_col)
435 base_q = base_q.offset(start).limit(limit)
435 base_q = base_q.offset(start).limit(limit)
436
436
437 ssh_keys = base_q.all()
437 ssh_keys = base_q.all()
438
438
439 ssh_keys_data = []
439 ssh_keys_data = []
440 for ssh_key in ssh_keys:
440 for ssh_key in ssh_keys:
441 ssh_keys_data.append({
441 ssh_keys_data.append({
442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
443 "fingerprint": ssh_key.ssh_key_fingerprint,
443 "fingerprint": ssh_key.ssh_key_fingerprint,
444 "description": ssh_key.description,
444 "description": ssh_key.description,
445 "created_on": h.format_date(ssh_key.created_on),
445 "created_on": h.format_date(ssh_key.created_on),
446 "accessed_on": h.format_date(ssh_key.accessed_on),
446 "accessed_on": h.format_date(ssh_key.accessed_on),
447 "action": h.link_to(
447 "action": h.link_to(
448 _('Edit'), h.route_path('edit_user_ssh_keys',
448 _('Edit'), h.route_path('edit_user_ssh_keys',
449 user_id=ssh_key.user.user_id))
449 user_id=ssh_key.user.user_id))
450 })
450 })
451
451
452 data = ({
452 data = ({
453 'draw': draw,
453 'draw': draw,
454 'data': ssh_keys_data,
454 'data': ssh_keys_data,
455 'recordsTotal': ssh_keys_data_total_count,
455 'recordsTotal': ssh_keys_data_total_count,
456 'recordsFiltered': users_data_total_filtered_count,
456 'recordsFiltered': users_data_total_filtered_count,
457 })
457 })
458
458
459 return data
459 return data
460
460
461 @LoginRequired()
461 @LoginRequired()
462 @HasPermissionAllDecorator('hg.admin')
462 @HasPermissionAllDecorator('hg.admin')
463 @CSRFRequired()
463 @CSRFRequired()
464 def ssh_keys_update(self):
464 def ssh_keys_update(self):
465 _ = self.request.translate
465 _ = self.request.translate
466 self.load_default_context()
466 self.load_default_context()
467
467
468 ssh_enabled = self.ssh_enabled()
468 ssh_enabled = self.ssh_enabled()
469 key_file = self.request.registry.settings.get(
469 key_file = self.request.registry.settings.get(
470 'ssh.authorized_keys_file_path')
470 'ssh.authorized_keys_file_path')
471 if ssh_enabled:
471 if ssh_enabled:
472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
473 h.flash(_('Updated SSH keys file: {}').format(key_file),
473 h.flash(_('Updated SSH keys file: {}').format(key_file),
474 category='success')
474 category='success')
475 else:
475 else:
476 h.flash(_('SSH key support is disabled in .ini file'),
476 h.flash(_('SSH key support is disabled in .ini file'),
477 category='warning')
477 category='warning')
478
478
479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,170 +1,170 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import psutil
23 import psutil
24 import signal
24 import signal
25
25
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps._base.navigation import navigation_list
28 from rhodecode.apps._base.navigation import navigation_list
29 from rhodecode.lib import system_info
29 from rhodecode.lib import system_info
30 from rhodecode.lib.auth import (
30 from rhodecode.lib.auth import (
31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
32 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class AdminProcessManagementView(BaseAppView):
37 class AdminProcessManagementView(BaseAppView):
38 def load_default_context(self):
38 def load_default_context(self):
39 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
40 return c
40 return c
41
41
42 def _format_proc(self, proc, with_children=False):
42 def _format_proc(self, proc, with_children=False):
43 try:
43 try:
44 mem = proc.memory_info()
44 mem = proc.memory_info()
45 proc_formatted = StrictAttributeDict({
45 proc_formatted = StrictAttributeDict({
46 'pid': proc.pid,
46 'pid': proc.pid,
47 'name': proc.name(),
47 'name': proc.name(),
48 'mem_rss': mem.rss,
48 'mem_rss': mem.rss,
49 'mem_vms': mem.vms,
49 'mem_vms': mem.vms,
50 'cpu_percent': proc.cpu_percent(interval=0.1),
50 'cpu_percent': proc.cpu_percent(interval=0.1),
51 'create_time': proc.create_time(),
51 'create_time': proc.create_time(),
52 'cmd': ' '.join(proc.cmdline()),
52 'cmd': ' '.join(proc.cmdline()),
53 })
53 })
54
54
55 if with_children:
55 if with_children:
56 proc_formatted.update({
56 proc_formatted.update({
57 'children': [self._format_proc(x)
57 'children': [self._format_proc(x)
58 for x in proc.children(recursive=True)]
58 for x in proc.children(recursive=True)]
59 })
59 })
60 except Exception:
60 except Exception:
61 log.exception('Failed to load proc')
61 log.exception('Failed to load proc')
62 proc_formatted = None
62 proc_formatted = None
63 return proc_formatted
63 return proc_formatted
64
64
65 def get_processes(self):
65 def get_processes(self):
66 proc_list = []
66 proc_list = []
67 for p in psutil.process_iter():
67 for p in psutil.process_iter():
68 if 'gunicorn' in p.name():
68 if 'gunicorn' in p.name():
69 proc = self._format_proc(p, with_children=True)
69 proc = self._format_proc(p, with_children=True)
70 if proc:
70 if proc:
71 proc_list.append(proc)
71 proc_list.append(proc)
72
72
73 return proc_list
73 return proc_list
74
74
75 def get_workers(self):
75 def get_workers(self):
76 workers = None
76 workers = None
77 try:
77 try:
78 rc_config = system_info.rhodecode_config().value['config']
78 rc_config = system_info.rhodecode_config().value['config']
79 workers = rc_config['server:main'].get('workers')
79 workers = rc_config['server:main'].get('workers')
80 except Exception:
80 except Exception:
81 pass
81 pass
82
82
83 return workers or '?'
83 return workers or '?'
84
84
85 @LoginRequired()
85 @LoginRequired()
86 @HasPermissionAllDecorator('hg.admin')
86 @HasPermissionAllDecorator('hg.admin')
87 def process_management(self):
87 def process_management(self):
88 _ = self.request.translate
88 _ = self.request.translate
89 c = self.load_default_context()
89 c = self.load_default_context()
90
90
91 c.active = 'process_management'
91 c.active = 'process_management'
92 c.navlist = navigation_list(self.request)
92 c.navlist = navigation_list(self.request)
93 c.gunicorn_processes = self.get_processes()
93 c.gunicorn_processes = self.get_processes()
94 c.gunicorn_workers = self.get_workers()
94 c.gunicorn_workers = self.get_workers()
95 return self._get_template_context(c)
95 return self._get_template_context(c)
96
96
97 @LoginRequired()
97 @LoginRequired()
98 @HasPermissionAllDecorator('hg.admin')
98 @HasPermissionAllDecorator('hg.admin')
99 def process_management_data(self):
99 def process_management_data(self):
100 _ = self.request.translate
100 _ = self.request.translate
101 c = self.load_default_context()
101 c = self.load_default_context()
102 c.gunicorn_processes = self.get_processes()
102 c.gunicorn_processes = self.get_processes()
103 return self._get_template_context(c)
103 return self._get_template_context(c)
104
104
105 @LoginRequired()
105 @LoginRequired()
106 @HasPermissionAllDecorator('hg.admin')
106 @HasPermissionAllDecorator('hg.admin')
107 @CSRFRequired()
107 @CSRFRequired()
108 def process_management_signal(self):
108 def process_management_signal(self):
109 pids = self.request.json.get('pids', [])
109 pids = self.request.json.get('pids', [])
110 result = []
110 result = []
111
111
112 def on_terminate(proc):
112 def on_terminate(proc):
113 msg = "terminated"
113 msg = "terminated"
114 result.append(msg)
114 result.append(msg)
115
115
116 procs = []
116 procs = []
117 for pid in pids:
117 for pid in pids:
118 pid = safe_int(pid)
118 pid = safe_int(pid)
119 if pid:
119 if pid:
120 try:
120 try:
121 proc = psutil.Process(pid)
121 proc = psutil.Process(pid)
122 except psutil.NoSuchProcess:
122 except psutil.NoSuchProcess:
123 continue
123 continue
124
124
125 children = proc.children(recursive=True)
125 children = proc.children(recursive=True)
126 if children:
126 if children:
127 log.warning('Wont kill Master Process')
127 log.warning('Wont kill Master Process')
128 else:
128 else:
129 procs.append(proc)
129 procs.append(proc)
130
130
131 for p in procs:
131 for p in procs:
132 try:
132 try:
133 p.terminate()
133 p.terminate()
134 except psutil.AccessDenied as e:
134 except psutil.AccessDenied as e:
135 log.warning('Access denied: {}'.format(e))
135 log.warning('Access denied: {}'.format(e))
136
136
137 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
137 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
138 for p in alive:
138 for p in alive:
139 try:
139 try:
140 p.kill()
140 p.kill()
141 except psutil.AccessDenied as e:
141 except psutil.AccessDenied as e:
142 log.warning('Access denied: {}'.format(e))
142 log.warning('Access denied: {}'.format(e))
143
143
144 return {'result': result}
144 return {'result': result}
145
145
146 @LoginRequired()
146 @LoginRequired()
147 @HasPermissionAllDecorator('hg.admin')
147 @HasPermissionAllDecorator('hg.admin')
148 @CSRFRequired()
148 @CSRFRequired()
149 def process_management_master_signal(self):
149 def process_management_master_signal(self):
150 pid_data = self.request.json.get('pid_data', {})
150 pid_data = self.request.json.get('pid_data', {})
151 pid = safe_int(pid_data['pid'])
151 pid = safe_int(pid_data['pid'])
152 action = pid_data['action']
152 action = pid_data['action']
153 if pid:
153 if pid:
154 try:
154 try:
155 proc = psutil.Process(pid)
155 proc = psutil.Process(pid)
156 except psutil.NoSuchProcess:
156 except psutil.NoSuchProcess:
157 return {'result': 'failure_no_such_process'}
157 return {'result': 'failure_no_such_process'}
158
158
159 children = proc.children(recursive=True)
159 children = proc.children(recursive=True)
160 if children:
160 if children:
161 # master process
161 # master process
162 if action == '+' and len(children) <= 20:
162 if action == '+' and len(children) <= 20:
163 proc.send_signal(signal.SIGTTIN)
163 proc.send_signal(signal.SIGTTIN)
164 elif action == '-' and len(children) >= 2:
164 elif action == '-' and len(children) >= 2:
165 proc.send_signal(signal.SIGTTOU)
165 proc.send_signal(signal.SIGTTOU)
166 else:
166 else:
167 return {'result': 'failure_wrong_action'}
167 return {'result': 'failure_wrong_action'}
168 return {'result': 'success'}
168 return {'result': 'success'}
169
169
170 return {'result': 'failure_not_master'}
170 return {'result': 'failure_not_master'}
@@ -1,356 +1,356 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import datetime
20 import datetime
21 import logging
21 import logging
22 import time
22 import time
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26
26
27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28
28
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode import events
32 from rhodecode import events
33 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34
34
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, CSRFRequired, NotAnonymous,
36 LoginRequired, CSRFRequired, NotAnonymous,
37 HasPermissionAny, HasRepoGroupPermissionAny)
37 HasPermissionAny, HasRepoGroupPermissionAny)
38 from rhodecode.lib import helpers as h, audit_logger
38 from rhodecode.lib import helpers as h, audit_logger
39 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
39 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
40 from rhodecode.model.forms import RepoGroupForm
40 from rhodecode.model.forms import RepoGroupForm
41 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
42 from rhodecode.model.repo_group import RepoGroupModel
42 from rhodecode.model.repo_group import RepoGroupModel
43 from rhodecode.model.scm import RepoGroupList
43 from rhodecode.model.scm import RepoGroupList
44 from rhodecode.model.db import (
44 from rhodecode.model.db import (
45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51
51
52 def load_default_context(self):
52 def load_default_context(self):
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54
54
55 return c
55 return c
56
56
57 def _load_form_data(self, c):
57 def _load_form_data(self, c):
58 allow_empty_group = False
58 allow_empty_group = False
59
59
60 if self._can_create_repo_group():
60 if self._can_create_repo_group():
61 # we're global admin, we're ok and we can create TOP level groups
61 # we're global admin, we're ok and we can create TOP level groups
62 allow_empty_group = True
62 allow_empty_group = True
63
63
64 # override the choices for this form, we need to filter choices
64 # override the choices for this form, we need to filter choices
65 # and display only those we have ADMIN right
65 # and display only those we have ADMIN right
66 groups_with_admin_rights = RepoGroupList(
66 groups_with_admin_rights = RepoGroupList(
67 RepoGroup.query().all(),
67 RepoGroup.query().all(),
68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 c.repo_groups = RepoGroup.groups_choices(
69 c.repo_groups = RepoGroup.groups_choices(
70 groups=groups_with_admin_rights,
70 groups=groups_with_admin_rights,
71 show_empty_group=allow_empty_group)
71 show_empty_group=allow_empty_group)
72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73
73
74 def _can_create_repo_group(self, parent_group_id=None):
74 def _can_create_repo_group(self, parent_group_id=None):
75 is_admin = HasPermissionAny('hg.admin')('group create controller')
75 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 create_repo_group = HasPermissionAny(
76 create_repo_group = HasPermissionAny(
77 'hg.repogroup.create.true')('group create controller')
77 'hg.repogroup.create.true')('group create controller')
78 if is_admin or (create_repo_group and not parent_group_id):
78 if is_admin or (create_repo_group and not parent_group_id):
79 # we're global admin, or we have global repo group create
79 # we're global admin, or we have global repo group create
80 # permission
80 # permission
81 # we're ok and we can create TOP level groups
81 # we're ok and we can create TOP level groups
82 return True
82 return True
83 elif parent_group_id:
83 elif parent_group_id:
84 # we check the permission if we can write to parent group
84 # we check the permission if we can write to parent group
85 group = RepoGroup.get(parent_group_id)
85 group = RepoGroup.get(parent_group_id)
86 group_name = group.group_name if group else None
86 group_name = group.group_name if group else None
87 if HasRepoGroupPermissionAny('group.admin')(
87 if HasRepoGroupPermissionAny('group.admin')(
88 group_name, 'check if user is an admin of group'):
88 group_name, 'check if user is an admin of group'):
89 # we're an admin of passed in group, we're ok.
89 # we're an admin of passed in group, we're ok.
90 return True
90 return True
91 else:
91 else:
92 return False
92 return False
93 return False
93 return False
94
94
95 # permission check in data loading of
95 # permission check in data loading of
96 # `repo_group_list_data` via RepoGroupList
96 # `repo_group_list_data` via RepoGroupList
97 @LoginRequired()
97 @LoginRequired()
98 @NotAnonymous()
98 @NotAnonymous()
99 def repo_group_list(self):
99 def repo_group_list(self):
100 c = self.load_default_context()
100 c = self.load_default_context()
101 return self._get_template_context(c)
101 return self._get_template_context(c)
102
102
103 # permission check inside
103 # permission check inside
104 @LoginRequired()
104 @LoginRequired()
105 @NotAnonymous()
105 @NotAnonymous()
106 def repo_group_list_data(self):
106 def repo_group_list_data(self):
107 self.load_default_context()
107 self.load_default_context()
108 column_map = {
108 column_map = {
109 'name': 'group_name_hash',
109 'name': 'group_name_hash',
110 'desc': 'group_description',
110 'desc': 'group_description',
111 'last_change': 'updated_on',
111 'last_change': 'updated_on',
112 'top_level_repos': 'repos_total',
112 'top_level_repos': 'repos_total',
113 'owner': 'user_username',
113 'owner': 'user_username',
114 }
114 }
115 draw, start, limit = self._extract_chunk(self.request)
115 draw, start, limit = self._extract_chunk(self.request)
116 search_q, order_by, order_dir = self._extract_ordering(
116 search_q, order_by, order_dir = self._extract_ordering(
117 self.request, column_map=column_map)
117 self.request, column_map=column_map)
118
118
119 _render = self.request.get_partial_renderer(
119 _render = self.request.get_partial_renderer(
120 'rhodecode:templates/data_table/_dt_elements.mako')
120 'rhodecode:templates/data_table/_dt_elements.mako')
121 c = _render.get_call_context()
121 c = _render.get_call_context()
122
122
123 def quick_menu(repo_group_name):
123 def quick_menu(repo_group_name):
124 return _render('quick_repo_group_menu', repo_group_name)
124 return _render('quick_repo_group_menu', repo_group_name)
125
125
126 def repo_group_lnk(repo_group_name):
126 def repo_group_lnk(repo_group_name):
127 return _render('repo_group_name', repo_group_name)
127 return _render('repo_group_name', repo_group_name)
128
128
129 def last_change(last_change):
129 def last_change(last_change):
130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
131 ts = time.time()
131 ts = time.time()
132 utc_offset = (datetime.datetime.fromtimestamp(ts)
132 utc_offset = (datetime.datetime.fromtimestamp(ts)
133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
135 return _render("last_change", last_change)
135 return _render("last_change", last_change)
136
136
137 def desc(desc, personal):
137 def desc(desc, personal):
138 return _render(
138 return _render(
139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
140
140
141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
142 return _render(
142 return _render(
143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
144
144
145 def user_profile(username):
145 def user_profile(username):
146 return _render('user_profile', username)
146 return _render('user_profile', username)
147
147
148 _perms = ['group.admin']
148 _perms = ['group.admin']
149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
150
150
151 repo_groups_data_total_count = RepoGroup.query()\
151 repo_groups_data_total_count = RepoGroup.query()\
152 .filter(or_(
152 .filter(or_(
153 # generate multiple IN to fix limitation problems
153 # generate multiple IN to fix limitation problems
154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
155 )) \
155 )) \
156 .count()
156 .count()
157
157
158 repo_groups_data_total_inactive_count = RepoGroup.query()\
158 repo_groups_data_total_inactive_count = RepoGroup.query()\
159 .filter(RepoGroup.group_id.in_(allowed_ids))\
159 .filter(RepoGroup.group_id.in_(allowed_ids))\
160 .count()
160 .count()
161
161
162 repo_count = count(Repository.repo_id)
162 repo_count = count(Repository.repo_id)
163 base_q = Session.query(
163 base_q = Session.query(
164 RepoGroup.group_name,
164 RepoGroup.group_name,
165 RepoGroup.group_name_hash,
165 RepoGroup.group_name_hash,
166 RepoGroup.group_description,
166 RepoGroup.group_description,
167 RepoGroup.group_id,
167 RepoGroup.group_id,
168 RepoGroup.personal,
168 RepoGroup.personal,
169 RepoGroup.updated_on,
169 RepoGroup.updated_on,
170 User,
170 User,
171 repo_count.label('repos_count')
171 repo_count.label('repos_count')
172 ) \
172 ) \
173 .filter(or_(
173 .filter(or_(
174 # generate multiple IN to fix limitation problems
174 # generate multiple IN to fix limitation problems
175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
176 )) \
176 )) \
177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
178 .join(User, User.user_id == RepoGroup.user_id) \
178 .join(User, User.user_id == RepoGroup.user_id) \
179 .group_by(RepoGroup, User)
179 .group_by(RepoGroup, User)
180
180
181 if search_q:
181 if search_q:
182 like_expression = u'%{}%'.format(safe_unicode(search_q))
182 like_expression = u'%{}%'.format(safe_unicode(search_q))
183 base_q = base_q.filter(or_(
183 base_q = base_q.filter(or_(
184 RepoGroup.group_name.ilike(like_expression),
184 RepoGroup.group_name.ilike(like_expression),
185 ))
185 ))
186
186
187 repo_groups_data_total_filtered_count = base_q.count()
187 repo_groups_data_total_filtered_count = base_q.count()
188 # the inactive isn't really used, but we still make it same as other data grids
188 # the inactive isn't really used, but we still make it same as other data grids
189 # which use inactive (users,user groups)
189 # which use inactive (users,user groups)
190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
191
191
192 sort_defined = False
192 sort_defined = False
193 if order_by == 'group_name':
193 if order_by == 'group_name':
194 sort_col = func.lower(RepoGroup.group_name)
194 sort_col = func.lower(RepoGroup.group_name)
195 sort_defined = True
195 sort_defined = True
196 elif order_by == 'repos_total':
196 elif order_by == 'repos_total':
197 sort_col = repo_count
197 sort_col = repo_count
198 sort_defined = True
198 sort_defined = True
199 elif order_by == 'user_username':
199 elif order_by == 'user_username':
200 sort_col = User.username
200 sort_col = User.username
201 else:
201 else:
202 sort_col = getattr(RepoGroup, order_by, None)
202 sort_col = getattr(RepoGroup, order_by, None)
203
203
204 if sort_defined or sort_col:
204 if sort_defined or sort_col:
205 if order_dir == 'asc':
205 if order_dir == 'asc':
206 sort_col = sort_col.asc()
206 sort_col = sort_col.asc()
207 else:
207 else:
208 sort_col = sort_col.desc()
208 sort_col = sort_col.desc()
209
209
210 base_q = base_q.order_by(sort_col)
210 base_q = base_q.order_by(sort_col)
211 base_q = base_q.offset(start).limit(limit)
211 base_q = base_q.offset(start).limit(limit)
212
212
213 # authenticated access to user groups
213 # authenticated access to user groups
214 auth_repo_group_list = base_q.all()
214 auth_repo_group_list = base_q.all()
215
215
216 repo_groups_data = []
216 repo_groups_data = []
217 for repo_gr in auth_repo_group_list:
217 for repo_gr in auth_repo_group_list:
218 row = {
218 row = {
219 "menu": quick_menu(repo_gr.group_name),
219 "menu": quick_menu(repo_gr.group_name),
220 "name": repo_group_lnk(repo_gr.group_name),
220 "name": repo_group_lnk(repo_gr.group_name),
221
221
222 "last_change": last_change(repo_gr.updated_on),
222 "last_change": last_change(repo_gr.updated_on),
223
223
224 "last_changeset": "",
224 "last_changeset": "",
225 "last_changeset_raw": "",
225 "last_changeset_raw": "",
226
226
227 "desc": desc(repo_gr.group_description, repo_gr.personal),
227 "desc": desc(repo_gr.group_description, repo_gr.personal),
228 "owner": user_profile(repo_gr.User.username),
228 "owner": user_profile(repo_gr.User.username),
229 "top_level_repos": repo_gr.repos_count,
229 "top_level_repos": repo_gr.repos_count,
230 "action": repo_group_actions(
230 "action": repo_group_actions(
231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
232
232
233 }
233 }
234
234
235 repo_groups_data.append(row)
235 repo_groups_data.append(row)
236
236
237 data = ({
237 data = ({
238 'draw': draw,
238 'draw': draw,
239 'data': repo_groups_data,
239 'data': repo_groups_data,
240 'recordsTotal': repo_groups_data_total_count,
240 'recordsTotal': repo_groups_data_total_count,
241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
242 'recordsFiltered': repo_groups_data_total_filtered_count,
242 'recordsFiltered': repo_groups_data_total_filtered_count,
243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
244 })
244 })
245
245
246 return data
246 return data
247
247
248 @LoginRequired()
248 @LoginRequired()
249 @NotAnonymous()
249 @NotAnonymous()
250 # perm checks inside
250 # perm checks inside
251 def repo_group_new(self):
251 def repo_group_new(self):
252 c = self.load_default_context()
252 c = self.load_default_context()
253
253
254 # perm check for admin, create_group perm or admin of parent_group
254 # perm check for admin, create_group perm or admin of parent_group
255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
256 _gr = RepoGroup.get(parent_group_id)
256 _gr = RepoGroup.get(parent_group_id)
257 if not self._can_create_repo_group(parent_group_id):
257 if not self._can_create_repo_group(parent_group_id):
258 raise HTTPForbidden()
258 raise HTTPForbidden()
259
259
260 self._load_form_data(c)
260 self._load_form_data(c)
261
261
262 defaults = {} # Future proof for default of repo group
262 defaults = {} # Future proof for default of repo group
263
263
264 parent_group_choice = '-1'
264 parent_group_choice = '-1'
265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
266 parent_group_choice = self._rhodecode_user.personal_repo_group
266 parent_group_choice = self._rhodecode_user.personal_repo_group
267
267
268 if parent_group_id and _gr:
268 if parent_group_id and _gr:
269 if parent_group_id in [x[0] for x in c.repo_groups]:
269 if parent_group_id in [x[0] for x in c.repo_groups]:
270 parent_group_choice = safe_unicode(parent_group_id)
270 parent_group_choice = safe_unicode(parent_group_id)
271
271
272 defaults.update({'group_parent_id': parent_group_choice})
272 defaults.update({'group_parent_id': parent_group_choice})
273
273
274 data = render(
274 data = render(
275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
276 self._get_template_context(c), self.request)
276 self._get_template_context(c), self.request)
277
277
278 html = formencode.htmlfill.render(
278 html = formencode.htmlfill.render(
279 data,
279 data,
280 defaults=defaults,
280 defaults=defaults,
281 encoding="UTF-8",
281 encoding="UTF-8",
282 force_defaults=False
282 force_defaults=False
283 )
283 )
284 return Response(html)
284 return Response(html)
285
285
286 @LoginRequired()
286 @LoginRequired()
287 @NotAnonymous()
287 @NotAnonymous()
288 @CSRFRequired()
288 @CSRFRequired()
289 # perm checks inside
289 # perm checks inside
290 def repo_group_create(self):
290 def repo_group_create(self):
291 c = self.load_default_context()
291 c = self.load_default_context()
292 _ = self.request.translate
292 _ = self.request.translate
293
293
294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
295 can_create = self._can_create_repo_group(parent_group_id)
295 can_create = self._can_create_repo_group(parent_group_id)
296
296
297 self._load_form_data(c)
297 self._load_form_data(c)
298 # permissions for can create group based on parent_id are checked
298 # permissions for can create group based on parent_id are checked
299 # here in the Form
299 # here in the Form
300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
301 repo_group_form = RepoGroupForm(
301 repo_group_form = RepoGroupForm(
302 self.request.translate, available_groups=available_groups,
302 self.request.translate, available_groups=available_groups,
303 can_create_in_root=can_create)()
303 can_create_in_root=can_create)()
304
304
305 repo_group_name = self.request.POST.get('group_name')
305 repo_group_name = self.request.POST.get('group_name')
306 try:
306 try:
307 owner = self._rhodecode_user
307 owner = self._rhodecode_user
308 form_result = repo_group_form.to_python(dict(self.request.POST))
308 form_result = repo_group_form.to_python(dict(self.request.POST))
309 copy_permissions = form_result.get('group_copy_permissions')
309 copy_permissions = form_result.get('group_copy_permissions')
310 repo_group = RepoGroupModel().create(
310 repo_group = RepoGroupModel().create(
311 group_name=form_result['group_name_full'],
311 group_name=form_result['group_name_full'],
312 group_description=form_result['group_description'],
312 group_description=form_result['group_description'],
313 owner=owner.user_id,
313 owner=owner.user_id,
314 copy_permissions=form_result['group_copy_permissions']
314 copy_permissions=form_result['group_copy_permissions']
315 )
315 )
316 Session().flush()
316 Session().flush()
317
317
318 repo_group_data = repo_group.get_api_data()
318 repo_group_data = repo_group.get_api_data()
319 audit_logger.store_web(
319 audit_logger.store_web(
320 'repo_group.create', action_data={'data': repo_group_data},
320 'repo_group.create', action_data={'data': repo_group_data},
321 user=self._rhodecode_user)
321 user=self._rhodecode_user)
322
322
323 Session().commit()
323 Session().commit()
324
324
325 _new_group_name = form_result['group_name_full']
325 _new_group_name = form_result['group_name_full']
326
326
327 repo_group_url = h.link_to(
327 repo_group_url = h.link_to(
328 _new_group_name,
328 _new_group_name,
329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
330 h.flash(h.literal(_('Created repository group %s')
330 h.flash(h.literal(_('Created repository group %s')
331 % repo_group_url), category='success')
331 % repo_group_url), category='success')
332
332
333 except formencode.Invalid as errors:
333 except formencode.Invalid as errors:
334 data = render(
334 data = render(
335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
336 self._get_template_context(c), self.request)
336 self._get_template_context(c), self.request)
337 html = formencode.htmlfill.render(
337 html = formencode.htmlfill.render(
338 data,
338 data,
339 defaults=errors.value,
339 defaults=errors.value,
340 errors=errors.unpack_errors() or {},
340 errors=errors.unpack_errors() or {},
341 prefix_error=False,
341 prefix_error=False,
342 encoding="UTF-8",
342 encoding="UTF-8",
343 force_defaults=False
343 force_defaults=False
344 )
344 )
345 return Response(html)
345 return Response(html)
346 except Exception:
346 except Exception:
347 log.exception("Exception during creation of repository group")
347 log.exception("Exception during creation of repository group")
348 h.flash(_('Error occurred during creation of repository group %s')
348 h.flash(_('Error occurred during creation of repository group %s')
349 % repo_group_name, category='error')
349 % repo_group_name, category='error')
350 raise HTTPFound(h.route_path('home'))
350 raise HTTPFound(h.route_path('home'))
351
351
352 PermissionModel().trigger_permission_flush()
352 PermissionModel().trigger_permission_flush()
353
353
354 raise HTTPFound(
354 raise HTTPFound(
355 h.route_path('repo_group_home',
355 h.route_path('repo_group_home',
356 repo_group_name=form_result['group_name_full']))
356 repo_group_name=form_result['group_name_full']))
@@ -1,250 +1,250 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import formencode
22 import formencode
23 import formencode.htmlfill
23 import formencode.htmlfill
24
24
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26
26
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.lib.celerylib.utils import get_task_id
32 from rhodecode.lib.celerylib.utils import get_task_id
33
33
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 LoginRequired, CSRFRequired, NotAnonymous,
35 LoginRequired, CSRFRequired, NotAnonymous,
36 HasPermissionAny, HasRepoGroupPermissionAny)
36 HasPermissionAny, HasRepoGroupPermissionAny)
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.utils import repo_name_slug
38 from rhodecode.lib.utils import repo_name_slug
39 from rhodecode.lib.utils2 import safe_int, safe_unicode
39 from rhodecode.lib.utils2 import safe_int, safe_unicode
40 from rhodecode.model.forms import RepoForm
40 from rhodecode.model.forms import RepoForm
41 from rhodecode.model.permission import PermissionModel
41 from rhodecode.model.permission import PermissionModel
42 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.db import (
45 from rhodecode.model.db import (
46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47
47
48 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
49
49
50
50
51 class AdminReposView(BaseAppView, DataGridAppView):
51 class AdminReposView(BaseAppView, DataGridAppView):
52
52
53 def load_default_context(self):
53 def load_default_context(self):
54 c = self._get_local_tmpl_context()
54 c = self._get_local_tmpl_context()
55 return c
55 return c
56
56
57 def _load_form_data(self, c):
57 def _load_form_data(self, c):
58 acl_groups = RepoGroupList(RepoGroup.query().all(),
58 acl_groups = RepoGroupList(RepoGroup.query().all(),
59 perm_set=['group.write', 'group.admin'])
59 perm_set=['group.write', 'group.admin'])
60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
63
63
64 @LoginRequired()
64 @LoginRequired()
65 @NotAnonymous()
65 @NotAnonymous()
66 # perms check inside
66 # perms check inside
67 def repository_list(self):
67 def repository_list(self):
68 c = self.load_default_context()
68 c = self.load_default_context()
69 return self._get_template_context(c)
69 return self._get_template_context(c)
70
70
71 @LoginRequired()
71 @LoginRequired()
72 @NotAnonymous()
72 @NotAnonymous()
73 # perms check inside
73 # perms check inside
74 def repository_list_data(self):
74 def repository_list_data(self):
75 self.load_default_context()
75 self.load_default_context()
76 column_map = {
76 column_map = {
77 'name': 'repo_name',
77 'name': 'repo_name',
78 'desc': 'description',
78 'desc': 'description',
79 'last_change': 'updated_on',
79 'last_change': 'updated_on',
80 'owner': 'user_username',
80 'owner': 'user_username',
81 }
81 }
82 draw, start, limit = self._extract_chunk(self.request)
82 draw, start, limit = self._extract_chunk(self.request)
83 search_q, order_by, order_dir = self._extract_ordering(
83 search_q, order_by, order_dir = self._extract_ordering(
84 self.request, column_map=column_map)
84 self.request, column_map=column_map)
85
85
86 _perms = ['repository.admin']
86 _perms = ['repository.admin']
87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
88
88
89 repos_data_total_count = Repository.query() \
89 repos_data_total_count = Repository.query() \
90 .filter(or_(
90 .filter(or_(
91 # generate multiple IN to fix limitation problems
91 # generate multiple IN to fix limitation problems
92 *in_filter_generator(Repository.repo_id, allowed_ids))
92 *in_filter_generator(Repository.repo_id, allowed_ids))
93 ) \
93 ) \
94 .count()
94 .count()
95
95
96 base_q = Session.query(
96 base_q = Session.query(
97 Repository.repo_id,
97 Repository.repo_id,
98 Repository.repo_name,
98 Repository.repo_name,
99 Repository.description,
99 Repository.description,
100 Repository.repo_type,
100 Repository.repo_type,
101 Repository.repo_state,
101 Repository.repo_state,
102 Repository.private,
102 Repository.private,
103 Repository.archived,
103 Repository.archived,
104 Repository.fork,
104 Repository.fork,
105 Repository.updated_on,
105 Repository.updated_on,
106 Repository._changeset_cache,
106 Repository._changeset_cache,
107 User,
107 User,
108 ) \
108 ) \
109 .filter(or_(
109 .filter(or_(
110 # generate multiple IN to fix limitation problems
110 # generate multiple IN to fix limitation problems
111 *in_filter_generator(Repository.repo_id, allowed_ids))
111 *in_filter_generator(Repository.repo_id, allowed_ids))
112 ) \
112 ) \
113 .join(User, User.user_id == Repository.user_id) \
113 .join(User, User.user_id == Repository.user_id) \
114 .group_by(Repository, User)
114 .group_by(Repository, User)
115
115
116 if search_q:
116 if search_q:
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 base_q = base_q.filter(or_(
118 base_q = base_q.filter(or_(
119 Repository.repo_name.ilike(like_expression),
119 Repository.repo_name.ilike(like_expression),
120 ))
120 ))
121
121
122 repos_data_total_filtered_count = base_q.count()
122 repos_data_total_filtered_count = base_q.count()
123
123
124 sort_defined = False
124 sort_defined = False
125 if order_by == 'repo_name':
125 if order_by == 'repo_name':
126 sort_col = func.lower(Repository.repo_name)
126 sort_col = func.lower(Repository.repo_name)
127 sort_defined = True
127 sort_defined = True
128 elif order_by == 'user_username':
128 elif order_by == 'user_username':
129 sort_col = User.username
129 sort_col = User.username
130 else:
130 else:
131 sort_col = getattr(Repository, order_by, None)
131 sort_col = getattr(Repository, order_by, None)
132
132
133 if sort_defined or sort_col:
133 if sort_defined or sort_col:
134 if order_dir == 'asc':
134 if order_dir == 'asc':
135 sort_col = sort_col.asc()
135 sort_col = sort_col.asc()
136 else:
136 else:
137 sort_col = sort_col.desc()
137 sort_col = sort_col.desc()
138
138
139 base_q = base_q.order_by(sort_col)
139 base_q = base_q.order_by(sort_col)
140 base_q = base_q.offset(start).limit(limit)
140 base_q = base_q.offset(start).limit(limit)
141
141
142 repos_list = base_q.all()
142 repos_list = base_q.all()
143
143
144 repos_data = RepoModel().get_repos_as_dict(
144 repos_data = RepoModel().get_repos_as_dict(
145 repo_list=repos_list, admin=True, super_user_actions=True)
145 repo_list=repos_list, admin=True, super_user_actions=True)
146
146
147 data = ({
147 data = ({
148 'draw': draw,
148 'draw': draw,
149 'data': repos_data,
149 'data': repos_data,
150 'recordsTotal': repos_data_total_count,
150 'recordsTotal': repos_data_total_count,
151 'recordsFiltered': repos_data_total_filtered_count,
151 'recordsFiltered': repos_data_total_filtered_count,
152 })
152 })
153 return data
153 return data
154
154
155 @LoginRequired()
155 @LoginRequired()
156 @NotAnonymous()
156 @NotAnonymous()
157 # perms check inside
157 # perms check inside
158 def repository_new(self):
158 def repository_new(self):
159 c = self.load_default_context()
159 c = self.load_default_context()
160
160
161 new_repo = self.request.GET.get('repo', '')
161 new_repo = self.request.GET.get('repo', '')
162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
163 _gr = RepoGroup.get(parent_group_id)
163 _gr = RepoGroup.get(parent_group_id)
164
164
165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
166 # you're not super admin nor have global create permissions,
166 # you're not super admin nor have global create permissions,
167 # but maybe you have at least write permission to a parent group ?
167 # but maybe you have at least write permission to a parent group ?
168
168
169 gr_name = _gr.group_name if _gr else None
169 gr_name = _gr.group_name if _gr else None
170 # create repositories with write permission on group is set to true
170 # create repositories with write permission on group is set to true
171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
174 if not (group_admin or (group_write and create_on_write)):
174 if not (group_admin or (group_write and create_on_write)):
175 raise HTTPForbidden()
175 raise HTTPForbidden()
176
176
177 self._load_form_data(c)
177 self._load_form_data(c)
178 c.new_repo = repo_name_slug(new_repo)
178 c.new_repo = repo_name_slug(new_repo)
179
179
180 # apply the defaults from defaults page
180 # apply the defaults from defaults page
181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
182 # set checkbox to autochecked
182 # set checkbox to autochecked
183 defaults['repo_copy_permissions'] = True
183 defaults['repo_copy_permissions'] = True
184
184
185 parent_group_choice = '-1'
185 parent_group_choice = '-1'
186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
187 parent_group_choice = self._rhodecode_user.personal_repo_group
187 parent_group_choice = self._rhodecode_user.personal_repo_group
188
188
189 if parent_group_id and _gr:
189 if parent_group_id and _gr:
190 if parent_group_id in [x[0] for x in c.repo_groups]:
190 if parent_group_id in [x[0] for x in c.repo_groups]:
191 parent_group_choice = safe_unicode(parent_group_id)
191 parent_group_choice = safe_unicode(parent_group_id)
192
192
193 defaults.update({'repo_group': parent_group_choice})
193 defaults.update({'repo_group': parent_group_choice})
194
194
195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
196 self._get_template_context(c), self.request)
196 self._get_template_context(c), self.request)
197 html = formencode.htmlfill.render(
197 html = formencode.htmlfill.render(
198 data,
198 data,
199 defaults=defaults,
199 defaults=defaults,
200 encoding="UTF-8",
200 encoding="UTF-8",
201 force_defaults=False
201 force_defaults=False
202 )
202 )
203 return Response(html)
203 return Response(html)
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
206 @NotAnonymous()
207 @CSRFRequired()
207 @CSRFRequired()
208 # perms check inside
208 # perms check inside
209 def repository_create(self):
209 def repository_create(self):
210 c = self.load_default_context()
210 c = self.load_default_context()
211
211
212 form_result = {}
212 form_result = {}
213 self._load_form_data(c)
213 self._load_form_data(c)
214
214
215 try:
215 try:
216 # CanWriteToGroup validators checks permissions of this POST
216 # CanWriteToGroup validators checks permissions of this POST
217 form = RepoForm(
217 form = RepoForm(
218 self.request.translate, repo_groups=c.repo_groups_choices)()
218 self.request.translate, repo_groups=c.repo_groups_choices)()
219 form_result = form.to_python(dict(self.request.POST))
219 form_result = form.to_python(dict(self.request.POST))
220 copy_permissions = form_result.get('repo_copy_permissions')
220 copy_permissions = form_result.get('repo_copy_permissions')
221 # create is done sometimes async on celery, db transaction
221 # create is done sometimes async on celery, db transaction
222 # management is handled there.
222 # management is handled there.
223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
224 task_id = get_task_id(task)
224 task_id = get_task_id(task)
225 except formencode.Invalid as errors:
225 except formencode.Invalid as errors:
226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
227 self._get_template_context(c), self.request)
227 self._get_template_context(c), self.request)
228 html = formencode.htmlfill.render(
228 html = formencode.htmlfill.render(
229 data,
229 data,
230 defaults=errors.value,
230 defaults=errors.value,
231 errors=errors.unpack_errors() or {},
231 errors=errors.unpack_errors() or {},
232 prefix_error=False,
232 prefix_error=False,
233 encoding="UTF-8",
233 encoding="UTF-8",
234 force_defaults=False
234 force_defaults=False
235 )
235 )
236 return Response(html)
236 return Response(html)
237
237
238 except Exception as e:
238 except Exception as e:
239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
240 h.flash(msg, category='error')
240 h.flash(msg, category='error')
241 raise HTTPFound(h.route_path('home'))
241 raise HTTPFound(h.route_path('home'))
242
242
243 repo_name = form_result.get('repo_name_full')
243 repo_name = form_result.get('repo_name_full')
244
244
245 affected_user_ids = [self._rhodecode_user.user_id]
245 affected_user_ids = [self._rhodecode_user.user_id]
246 PermissionModel().trigger_permission_flush(affected_user_ids)
246 PermissionModel().trigger_permission_flush(affected_user_ids)
247
247
248 raise HTTPFound(
248 raise HTTPFound(
249 h.route_path('repo_creating', repo_name=repo_name,
249 h.route_path('repo_creating', repo_name=repo_name,
250 _query=dict(task_id=task_id)))
250 _query=dict(task_id=task_id)))
@@ -1,95 +1,95 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23
23
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base.navigation import navigation_list
27 from rhodecode.apps._base.navigation import navigation_list
28 from rhodecode.lib.auth import (
28 from rhodecode.lib.auth import (
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 from rhodecode.lib.utils2 import safe_int
30 from rhodecode.lib.utils2 import safe_int
31 from rhodecode.lib import system_info
31 from rhodecode.lib import system_info
32 from rhodecode.lib import user_sessions
32 from rhodecode.lib import user_sessions
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34
34
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38
38
39 class AdminSessionSettingsView(BaseAppView):
39 class AdminSessionSettingsView(BaseAppView):
40
40
41 def load_default_context(self):
41 def load_default_context(self):
42 c = self._get_local_tmpl_context()
42 c = self._get_local_tmpl_context()
43 return c
43 return c
44
44
45 @LoginRequired()
45 @LoginRequired()
46 @HasPermissionAllDecorator('hg.admin')
46 @HasPermissionAllDecorator('hg.admin')
47 def settings_sessions(self):
47 def settings_sessions(self):
48 c = self.load_default_context()
48 c = self.load_default_context()
49
49
50 c.active = 'sessions'
50 c.active = 'sessions'
51 c.navlist = navigation_list(self.request)
51 c.navlist = navigation_list(self.request)
52
52
53 c.cleanup_older_days = 60
53 c.cleanup_older_days = 60
54 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
54 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
55
55
56 config = system_info.rhodecode_config().get_value()['value']['config']
56 config = system_info.rhodecode_config().get_value()['value']['config']
57 c.session_model = user_sessions.get_session_handler(
57 c.session_model = user_sessions.get_session_handler(
58 config.get('beaker.session.type', 'memory'))(config)
58 config.get('beaker.session.type', 'memory'))(config)
59
59
60 c.session_conf = c.session_model.config
60 c.session_conf = c.session_model.config
61 c.session_count = c.session_model.get_count()
61 c.session_count = c.session_model.get_count()
62 c.session_expired_count = c.session_model.get_expired_count(
62 c.session_expired_count = c.session_model.get_expired_count(
63 older_than_seconds)
63 older_than_seconds)
64
64
65 return self._get_template_context(c)
65 return self._get_template_context(c)
66
66
67 @LoginRequired()
67 @LoginRequired()
68 @HasPermissionAllDecorator('hg.admin')
68 @HasPermissionAllDecorator('hg.admin')
69 @CSRFRequired()
69 @CSRFRequired()
70 def settings_sessions_cleanup(self):
70 def settings_sessions_cleanup(self):
71 _ = self.request.translate
71 _ = self.request.translate
72 expire_days = safe_int(self.request.params.get('expire_days'))
72 expire_days = safe_int(self.request.params.get('expire_days'))
73
73
74 if expire_days is None:
74 if expire_days is None:
75 expire_days = 60
75 expire_days = 60
76
76
77 older_than_seconds = 60 * 60 * 24 * expire_days
77 older_than_seconds = 60 * 60 * 24 * expire_days
78
78
79 config = system_info.rhodecode_config().get_value()['value']['config']
79 config = system_info.rhodecode_config().get_value()['value']['config']
80 session_model = user_sessions.get_session_handler(
80 session_model = user_sessions.get_session_handler(
81 config.get('beaker.session.type', 'memory'))(config)
81 config.get('beaker.session.type', 'memory'))(config)
82
82
83 try:
83 try:
84 session_model.clean_sessions(
84 session_model.clean_sessions(
85 older_than_seconds=older_than_seconds)
85 older_than_seconds=older_than_seconds)
86 h.flash(_('Cleaned up old sessions'), category='success')
86 h.flash(_('Cleaned up old sessions'), category='success')
87 except user_sessions.CleanupCommand as msg:
87 except user_sessions.CleanupCommand as msg:
88 h.flash(msg.message, category='warning')
88 h.flash(msg.message, category='warning')
89 except Exception as e:
89 except Exception as e:
90 log.exception('Failed session cleanup')
90 log.exception('Failed session cleanup')
91 h.flash(_('Failed to cleanup up old sessions'), category='error')
91 h.flash(_('Failed to cleanup up old sessions'), category='error')
92
92
93 redirect_to = self.request.resource_path(
93 redirect_to = self.request.resource_path(
94 self.context, route_name='admin_settings_sessions')
94 self.context, route_name='admin_settings_sessions')
95 return HTTPFound(redirect_to)
95 return HTTPFound(redirect_to)
@@ -1,720 +1,719 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21
20
22 import logging
21 import logging
23 import collections
22 import collections
24
23
25 import datetime
24 import datetime
26 import formencode
25 import formencode
27 import formencode.htmlfill
26 import formencode.htmlfill
28
27
29 import rhodecode
28 import rhodecode
30
29
31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
30 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 from pyramid.renderers import render
31 from pyramid.renderers import render
33 from pyramid.response import Response
32 from pyramid.response import Response
34
33
35 from rhodecode.apps._base import BaseAppView
34 from rhodecode.apps._base import BaseAppView
36 from rhodecode.apps._base.navigation import navigation_list
35 from rhodecode.apps._base.navigation import navigation_list
37 from rhodecode.apps.svn_support.config_keys import generate_config
36 from rhodecode.apps.svn_support.config_keys import generate_config
38 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
39 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
39 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 from rhodecode.lib.celerylib import tasks, run_task
40 from rhodecode.lib.celerylib import tasks, run_task
42 from rhodecode.lib.utils import repo2db_mapper
41 from rhodecode.lib.utils import repo2db_mapper
43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
42 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 from rhodecode.lib.index import searcher_from_config
43 from rhodecode.lib.index import searcher_from_config
45
44
46 from rhodecode.model.db import RhodeCodeUi, Repository
45 from rhodecode.model.db import RhodeCodeUi, Repository
47 from rhodecode.model.forms import (ApplicationSettingsForm,
46 from rhodecode.model.forms import (ApplicationSettingsForm,
48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
47 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 LabsSettingsForm, IssueTrackerPatternsForm)
48 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.permission import PermissionModel
49 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.repo_group import RepoGroupModel
50 from rhodecode.model.repo_group import RepoGroupModel
52
51
53 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
54 from rhodecode.model.notification import EmailNotificationModel
53 from rhodecode.model.notification import EmailNotificationModel
55 from rhodecode.model.meta import Session
54 from rhodecode.model.meta import Session
56 from rhodecode.model.settings import (
55 from rhodecode.model.settings import (
57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
56 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 SettingsModel)
57 SettingsModel)
59
58
60
59
61 log = logging.getLogger(__name__)
60 log = logging.getLogger(__name__)
62
61
63
62
64 class AdminSettingsView(BaseAppView):
63 class AdminSettingsView(BaseAppView):
65
64
66 def load_default_context(self):
65 def load_default_context(self):
67 c = self._get_local_tmpl_context()
66 c = self._get_local_tmpl_context()
68 c.labs_active = str2bool(
67 c.labs_active = str2bool(
69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
68 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 c.navlist = navigation_list(self.request)
69 c.navlist = navigation_list(self.request)
71 return c
70 return c
72
71
73 @classmethod
72 @classmethod
74 def _get_ui_settings(cls):
73 def _get_ui_settings(cls):
75 ret = RhodeCodeUi.query().all()
74 ret = RhodeCodeUi.query().all()
76
75
77 if not ret:
76 if not ret:
78 raise Exception('Could not get application ui settings !')
77 raise Exception('Could not get application ui settings !')
79 settings = {}
78 settings = {}
80 for each in ret:
79 for each in ret:
81 k = each.ui_key
80 k = each.ui_key
82 v = each.ui_value
81 v = each.ui_value
83 if k == '/':
82 if k == '/':
84 k = 'root_path'
83 k = 'root_path'
85
84
86 if k in ['push_ssl', 'publish', 'enabled']:
85 if k in ['push_ssl', 'publish', 'enabled']:
87 v = str2bool(v)
86 v = str2bool(v)
88
87
89 if k.find('.') != -1:
88 if k.find('.') != -1:
90 k = k.replace('.', '_')
89 k = k.replace('.', '_')
91
90
92 if each.ui_section in ['hooks', 'extensions']:
91 if each.ui_section in ['hooks', 'extensions']:
93 v = each.ui_active
92 v = each.ui_active
94
93
95 settings[each.ui_section + '_' + k] = v
94 settings[each.ui_section + '_' + k] = v
96 return settings
95 return settings
97
96
98 @classmethod
97 @classmethod
99 def _form_defaults(cls):
98 def _form_defaults(cls):
100 defaults = SettingsModel().get_all_settings()
99 defaults = SettingsModel().get_all_settings()
101 defaults.update(cls._get_ui_settings())
100 defaults.update(cls._get_ui_settings())
102
101
103 defaults.update({
102 defaults.update({
104 'new_svn_branch': '',
103 'new_svn_branch': '',
105 'new_svn_tag': '',
104 'new_svn_tag': '',
106 })
105 })
107 return defaults
106 return defaults
108
107
109 @LoginRequired()
108 @LoginRequired()
110 @HasPermissionAllDecorator('hg.admin')
109 @HasPermissionAllDecorator('hg.admin')
111 def settings_vcs(self):
110 def settings_vcs(self):
112 c = self.load_default_context()
111 c = self.load_default_context()
113 c.active = 'vcs'
112 c.active = 'vcs'
114 model = VcsSettingsModel()
113 model = VcsSettingsModel()
115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
114 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
115 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
117
116
118 settings = self.request.registry.settings
117 settings = self.request.registry.settings
119 c.svn_proxy_generate_config = settings[generate_config]
118 c.svn_proxy_generate_config = settings[generate_config]
120
119
121 defaults = self._form_defaults()
120 defaults = self._form_defaults()
122
121
123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
122 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
124
123
125 data = render('rhodecode:templates/admin/settings/settings.mako',
124 data = render('rhodecode:templates/admin/settings/settings.mako',
126 self._get_template_context(c), self.request)
125 self._get_template_context(c), self.request)
127 html = formencode.htmlfill.render(
126 html = formencode.htmlfill.render(
128 data,
127 data,
129 defaults=defaults,
128 defaults=defaults,
130 encoding="UTF-8",
129 encoding="UTF-8",
131 force_defaults=False
130 force_defaults=False
132 )
131 )
133 return Response(html)
132 return Response(html)
134
133
135 @LoginRequired()
134 @LoginRequired()
136 @HasPermissionAllDecorator('hg.admin')
135 @HasPermissionAllDecorator('hg.admin')
137 @CSRFRequired()
136 @CSRFRequired()
138 def settings_vcs_update(self):
137 def settings_vcs_update(self):
139 _ = self.request.translate
138 _ = self.request.translate
140 c = self.load_default_context()
139 c = self.load_default_context()
141 c.active = 'vcs'
140 c.active = 'vcs'
142
141
143 model = VcsSettingsModel()
142 model = VcsSettingsModel()
144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
143 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
144 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
146
145
147 settings = self.request.registry.settings
146 settings = self.request.registry.settings
148 c.svn_proxy_generate_config = settings[generate_config]
147 c.svn_proxy_generate_config = settings[generate_config]
149
148
150 application_form = ApplicationUiSettingsForm(self.request.translate)()
149 application_form = ApplicationUiSettingsForm(self.request.translate)()
151
150
152 try:
151 try:
153 form_result = application_form.to_python(dict(self.request.POST))
152 form_result = application_form.to_python(dict(self.request.POST))
154 except formencode.Invalid as errors:
153 except formencode.Invalid as errors:
155 h.flash(
154 h.flash(
156 _("Some form inputs contain invalid data."),
155 _("Some form inputs contain invalid data."),
157 category='error')
156 category='error')
158 data = render('rhodecode:templates/admin/settings/settings.mako',
157 data = render('rhodecode:templates/admin/settings/settings.mako',
159 self._get_template_context(c), self.request)
158 self._get_template_context(c), self.request)
160 html = formencode.htmlfill.render(
159 html = formencode.htmlfill.render(
161 data,
160 data,
162 defaults=errors.value,
161 defaults=errors.value,
163 errors=errors.unpack_errors() or {},
162 errors=errors.unpack_errors() or {},
164 prefix_error=False,
163 prefix_error=False,
165 encoding="UTF-8",
164 encoding="UTF-8",
166 force_defaults=False
165 force_defaults=False
167 )
166 )
168 return Response(html)
167 return Response(html)
169
168
170 try:
169 try:
171 if c.visual.allow_repo_location_change:
170 if c.visual.allow_repo_location_change:
172 model.update_global_path_setting(form_result['paths_root_path'])
171 model.update_global_path_setting(form_result['paths_root_path'])
173
172
174 model.update_global_ssl_setting(form_result['web_push_ssl'])
173 model.update_global_ssl_setting(form_result['web_push_ssl'])
175 model.update_global_hook_settings(form_result)
174 model.update_global_hook_settings(form_result)
176
175
177 model.create_or_update_global_svn_settings(form_result)
176 model.create_or_update_global_svn_settings(form_result)
178 model.create_or_update_global_hg_settings(form_result)
177 model.create_or_update_global_hg_settings(form_result)
179 model.create_or_update_global_git_settings(form_result)
178 model.create_or_update_global_git_settings(form_result)
180 model.create_or_update_global_pr_settings(form_result)
179 model.create_or_update_global_pr_settings(form_result)
181 except Exception:
180 except Exception:
182 log.exception("Exception while updating settings")
181 log.exception("Exception while updating settings")
183 h.flash(_('Error occurred during updating '
182 h.flash(_('Error occurred during updating '
184 'application settings'), category='error')
183 'application settings'), category='error')
185 else:
184 else:
186 Session().commit()
185 Session().commit()
187 h.flash(_('Updated VCS settings'), category='success')
186 h.flash(_('Updated VCS settings'), category='success')
188 raise HTTPFound(h.route_path('admin_settings_vcs'))
187 raise HTTPFound(h.route_path('admin_settings_vcs'))
189
188
190 data = render('rhodecode:templates/admin/settings/settings.mako',
189 data = render('rhodecode:templates/admin/settings/settings.mako',
191 self._get_template_context(c), self.request)
190 self._get_template_context(c), self.request)
192 html = formencode.htmlfill.render(
191 html = formencode.htmlfill.render(
193 data,
192 data,
194 defaults=self._form_defaults(),
193 defaults=self._form_defaults(),
195 encoding="UTF-8",
194 encoding="UTF-8",
196 force_defaults=False
195 force_defaults=False
197 )
196 )
198 return Response(html)
197 return Response(html)
199
198
200 @LoginRequired()
199 @LoginRequired()
201 @HasPermissionAllDecorator('hg.admin')
200 @HasPermissionAllDecorator('hg.admin')
202 @CSRFRequired()
201 @CSRFRequired()
203 def settings_vcs_delete_svn_pattern(self):
202 def settings_vcs_delete_svn_pattern(self):
204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
203 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
205 model = VcsSettingsModel()
204 model = VcsSettingsModel()
206 try:
205 try:
207 model.delete_global_svn_pattern(delete_pattern_id)
206 model.delete_global_svn_pattern(delete_pattern_id)
208 except SettingNotFound:
207 except SettingNotFound:
209 log.exception(
208 log.exception(
210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
209 'Failed to delete svn_pattern with id %s', delete_pattern_id)
211 raise HTTPNotFound()
210 raise HTTPNotFound()
212
211
213 Session().commit()
212 Session().commit()
214 return True
213 return True
215
214
216 @LoginRequired()
215 @LoginRequired()
217 @HasPermissionAllDecorator('hg.admin')
216 @HasPermissionAllDecorator('hg.admin')
218 def settings_mapping(self):
217 def settings_mapping(self):
219 c = self.load_default_context()
218 c = self.load_default_context()
220 c.active = 'mapping'
219 c.active = 'mapping'
221
220
222 data = render('rhodecode:templates/admin/settings/settings.mako',
221 data = render('rhodecode:templates/admin/settings/settings.mako',
223 self._get_template_context(c), self.request)
222 self._get_template_context(c), self.request)
224 html = formencode.htmlfill.render(
223 html = formencode.htmlfill.render(
225 data,
224 data,
226 defaults=self._form_defaults(),
225 defaults=self._form_defaults(),
227 encoding="UTF-8",
226 encoding="UTF-8",
228 force_defaults=False
227 force_defaults=False
229 )
228 )
230 return Response(html)
229 return Response(html)
231
230
232 @LoginRequired()
231 @LoginRequired()
233 @HasPermissionAllDecorator('hg.admin')
232 @HasPermissionAllDecorator('hg.admin')
234 @CSRFRequired()
233 @CSRFRequired()
235 def settings_mapping_update(self):
234 def settings_mapping_update(self):
236 _ = self.request.translate
235 _ = self.request.translate
237 c = self.load_default_context()
236 c = self.load_default_context()
238 c.active = 'mapping'
237 c.active = 'mapping'
239 rm_obsolete = self.request.POST.get('destroy', False)
238 rm_obsolete = self.request.POST.get('destroy', False)
240 invalidate_cache = self.request.POST.get('invalidate', False)
239 invalidate_cache = self.request.POST.get('invalidate', False)
241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
240 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
242
241
243 if invalidate_cache:
242 if invalidate_cache:
244 log.debug('invalidating all repositories cache')
243 log.debug('invalidating all repositories cache')
245 for repo in Repository.get_all():
244 for repo in Repository.get_all():
246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
245 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
247
246
248 filesystem_repos = ScmModel().repo_scan()
247 filesystem_repos = ScmModel().repo_scan()
249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
248 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
250 PermissionModel().trigger_permission_flush()
249 PermissionModel().trigger_permission_flush()
251
250
252 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
251 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
253 h.flash(_('Repositories successfully '
252 h.flash(_('Repositories successfully '
254 'rescanned added: %s ; removed: %s') %
253 'rescanned added: %s ; removed: %s') %
255 (_repr(added), _repr(removed)),
254 (_repr(added), _repr(removed)),
256 category='success')
255 category='success')
257 raise HTTPFound(h.route_path('admin_settings_mapping'))
256 raise HTTPFound(h.route_path('admin_settings_mapping'))
258
257
259 @LoginRequired()
258 @LoginRequired()
260 @HasPermissionAllDecorator('hg.admin')
259 @HasPermissionAllDecorator('hg.admin')
261 def settings_global(self):
260 def settings_global(self):
262 c = self.load_default_context()
261 c = self.load_default_context()
263 c.active = 'global'
262 c.active = 'global'
264 c.personal_repo_group_default_pattern = RepoGroupModel()\
263 c.personal_repo_group_default_pattern = RepoGroupModel()\
265 .get_personal_group_name_pattern()
264 .get_personal_group_name_pattern()
266
265
267 data = render('rhodecode:templates/admin/settings/settings.mako',
266 data = render('rhodecode:templates/admin/settings/settings.mako',
268 self._get_template_context(c), self.request)
267 self._get_template_context(c), self.request)
269 html = formencode.htmlfill.render(
268 html = formencode.htmlfill.render(
270 data,
269 data,
271 defaults=self._form_defaults(),
270 defaults=self._form_defaults(),
272 encoding="UTF-8",
271 encoding="UTF-8",
273 force_defaults=False
272 force_defaults=False
274 )
273 )
275 return Response(html)
274 return Response(html)
276
275
277 @LoginRequired()
276 @LoginRequired()
278 @HasPermissionAllDecorator('hg.admin')
277 @HasPermissionAllDecorator('hg.admin')
279 @CSRFRequired()
278 @CSRFRequired()
280 def settings_global_update(self):
279 def settings_global_update(self):
281 _ = self.request.translate
280 _ = self.request.translate
282 c = self.load_default_context()
281 c = self.load_default_context()
283 c.active = 'global'
282 c.active = 'global'
284 c.personal_repo_group_default_pattern = RepoGroupModel()\
283 c.personal_repo_group_default_pattern = RepoGroupModel()\
285 .get_personal_group_name_pattern()
284 .get_personal_group_name_pattern()
286 application_form = ApplicationSettingsForm(self.request.translate)()
285 application_form = ApplicationSettingsForm(self.request.translate)()
287 try:
286 try:
288 form_result = application_form.to_python(dict(self.request.POST))
287 form_result = application_form.to_python(dict(self.request.POST))
289 except formencode.Invalid as errors:
288 except formencode.Invalid as errors:
290 h.flash(
289 h.flash(
291 _("Some form inputs contain invalid data."),
290 _("Some form inputs contain invalid data."),
292 category='error')
291 category='error')
293 data = render('rhodecode:templates/admin/settings/settings.mako',
292 data = render('rhodecode:templates/admin/settings/settings.mako',
294 self._get_template_context(c), self.request)
293 self._get_template_context(c), self.request)
295 html = formencode.htmlfill.render(
294 html = formencode.htmlfill.render(
296 data,
295 data,
297 defaults=errors.value,
296 defaults=errors.value,
298 errors=errors.unpack_errors() or {},
297 errors=errors.unpack_errors() or {},
299 prefix_error=False,
298 prefix_error=False,
300 encoding="UTF-8",
299 encoding="UTF-8",
301 force_defaults=False
300 force_defaults=False
302 )
301 )
303 return Response(html)
302 return Response(html)
304
303
305 settings = [
304 settings = [
306 ('title', 'rhodecode_title', 'unicode'),
305 ('title', 'rhodecode_title', 'unicode'),
307 ('realm', 'rhodecode_realm', 'unicode'),
306 ('realm', 'rhodecode_realm', 'unicode'),
308 ('pre_code', 'rhodecode_pre_code', 'unicode'),
307 ('pre_code', 'rhodecode_pre_code', 'unicode'),
309 ('post_code', 'rhodecode_post_code', 'unicode'),
308 ('post_code', 'rhodecode_post_code', 'unicode'),
310 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
309 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
311 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
310 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
312 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
311 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
313 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
312 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
314 ]
313 ]
315 try:
314 try:
316 for setting, form_key, type_ in settings:
315 for setting, form_key, type_ in settings:
317 sett = SettingsModel().create_or_update_setting(
316 sett = SettingsModel().create_or_update_setting(
318 setting, form_result[form_key], type_)
317 setting, form_result[form_key], type_)
319 Session().add(sett)
318 Session().add(sett)
320
319
321 Session().commit()
320 Session().commit()
322 SettingsModel().invalidate_settings_cache()
321 SettingsModel().invalidate_settings_cache()
323 h.flash(_('Updated application settings'), category='success')
322 h.flash(_('Updated application settings'), category='success')
324 except Exception:
323 except Exception:
325 log.exception("Exception while updating application settings")
324 log.exception("Exception while updating application settings")
326 h.flash(
325 h.flash(
327 _('Error occurred during updating application settings'),
326 _('Error occurred during updating application settings'),
328 category='error')
327 category='error')
329
328
330 raise HTTPFound(h.route_path('admin_settings_global'))
329 raise HTTPFound(h.route_path('admin_settings_global'))
331
330
332 @LoginRequired()
331 @LoginRequired()
333 @HasPermissionAllDecorator('hg.admin')
332 @HasPermissionAllDecorator('hg.admin')
334 def settings_visual(self):
333 def settings_visual(self):
335 c = self.load_default_context()
334 c = self.load_default_context()
336 c.active = 'visual'
335 c.active = 'visual'
337
336
338 data = render('rhodecode:templates/admin/settings/settings.mako',
337 data = render('rhodecode:templates/admin/settings/settings.mako',
339 self._get_template_context(c), self.request)
338 self._get_template_context(c), self.request)
340 html = formencode.htmlfill.render(
339 html = formencode.htmlfill.render(
341 data,
340 data,
342 defaults=self._form_defaults(),
341 defaults=self._form_defaults(),
343 encoding="UTF-8",
342 encoding="UTF-8",
344 force_defaults=False
343 force_defaults=False
345 )
344 )
346 return Response(html)
345 return Response(html)
347
346
348 @LoginRequired()
347 @LoginRequired()
349 @HasPermissionAllDecorator('hg.admin')
348 @HasPermissionAllDecorator('hg.admin')
350 @CSRFRequired()
349 @CSRFRequired()
351 def settings_visual_update(self):
350 def settings_visual_update(self):
352 _ = self.request.translate
351 _ = self.request.translate
353 c = self.load_default_context()
352 c = self.load_default_context()
354 c.active = 'visual'
353 c.active = 'visual'
355 application_form = ApplicationVisualisationForm(self.request.translate)()
354 application_form = ApplicationVisualisationForm(self.request.translate)()
356 try:
355 try:
357 form_result = application_form.to_python(dict(self.request.POST))
356 form_result = application_form.to_python(dict(self.request.POST))
358 except formencode.Invalid as errors:
357 except formencode.Invalid as errors:
359 h.flash(
358 h.flash(
360 _("Some form inputs contain invalid data."),
359 _("Some form inputs contain invalid data."),
361 category='error')
360 category='error')
362 data = render('rhodecode:templates/admin/settings/settings.mako',
361 data = render('rhodecode:templates/admin/settings/settings.mako',
363 self._get_template_context(c), self.request)
362 self._get_template_context(c), self.request)
364 html = formencode.htmlfill.render(
363 html = formencode.htmlfill.render(
365 data,
364 data,
366 defaults=errors.value,
365 defaults=errors.value,
367 errors=errors.unpack_errors() or {},
366 errors=errors.unpack_errors() or {},
368 prefix_error=False,
367 prefix_error=False,
369 encoding="UTF-8",
368 encoding="UTF-8",
370 force_defaults=False
369 force_defaults=False
371 )
370 )
372 return Response(html)
371 return Response(html)
373
372
374 try:
373 try:
375 settings = [
374 settings = [
376 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
375 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
377 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
376 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
378 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
377 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
379 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
378 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
380 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
379 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
381 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
380 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
382 ('show_version', 'rhodecode_show_version', 'bool'),
381 ('show_version', 'rhodecode_show_version', 'bool'),
383 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
382 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
384 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
383 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
385 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
384 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
386 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
385 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
387 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
386 ('clone_uri_id_tmpl', 'rhodecode_clone_uri_id_tmpl', 'unicode'),
388 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
387 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
389 ('support_url', 'rhodecode_support_url', 'unicode'),
388 ('support_url', 'rhodecode_support_url', 'unicode'),
390 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
389 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
391 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
390 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
392 ]
391 ]
393 for setting, form_key, type_ in settings:
392 for setting, form_key, type_ in settings:
394 sett = SettingsModel().create_or_update_setting(
393 sett = SettingsModel().create_or_update_setting(
395 setting, form_result[form_key], type_)
394 setting, form_result[form_key], type_)
396 Session().add(sett)
395 Session().add(sett)
397
396
398 Session().commit()
397 Session().commit()
399 SettingsModel().invalidate_settings_cache()
398 SettingsModel().invalidate_settings_cache()
400 h.flash(_('Updated visualisation settings'), category='success')
399 h.flash(_('Updated visualisation settings'), category='success')
401 except Exception:
400 except Exception:
402 log.exception("Exception updating visualization settings")
401 log.exception("Exception updating visualization settings")
403 h.flash(_('Error occurred during updating '
402 h.flash(_('Error occurred during updating '
404 'visualisation settings'),
403 'visualisation settings'),
405 category='error')
404 category='error')
406
405
407 raise HTTPFound(h.route_path('admin_settings_visual'))
406 raise HTTPFound(h.route_path('admin_settings_visual'))
408
407
409 @LoginRequired()
408 @LoginRequired()
410 @HasPermissionAllDecorator('hg.admin')
409 @HasPermissionAllDecorator('hg.admin')
411 def settings_issuetracker(self):
410 def settings_issuetracker(self):
412 c = self.load_default_context()
411 c = self.load_default_context()
413 c.active = 'issuetracker'
412 c.active = 'issuetracker'
414 defaults = c.rc_config
413 defaults = c.rc_config
415
414
416 entry_key = 'rhodecode_issuetracker_pat_'
415 entry_key = 'rhodecode_issuetracker_pat_'
417
416
418 c.issuetracker_entries = {}
417 c.issuetracker_entries = {}
419 for k, v in defaults.items():
418 for k, v in defaults.items():
420 if k.startswith(entry_key):
419 if k.startswith(entry_key):
421 uid = k[len(entry_key):]
420 uid = k[len(entry_key):]
422 c.issuetracker_entries[uid] = None
421 c.issuetracker_entries[uid] = None
423
422
424 for uid in c.issuetracker_entries:
423 for uid in c.issuetracker_entries:
425 c.issuetracker_entries[uid] = AttributeDict({
424 c.issuetracker_entries[uid] = AttributeDict({
426 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
425 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
427 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
426 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
428 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
427 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
429 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
428 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
430 })
429 })
431
430
432 return self._get_template_context(c)
431 return self._get_template_context(c)
433
432
434 @LoginRequired()
433 @LoginRequired()
435 @HasPermissionAllDecorator('hg.admin')
434 @HasPermissionAllDecorator('hg.admin')
436 @CSRFRequired()
435 @CSRFRequired()
437 def settings_issuetracker_test(self):
436 def settings_issuetracker_test(self):
438 error_container = []
437 error_container = []
439
438
440 urlified_commit = h.urlify_commit_message(
439 urlified_commit = h.urlify_commit_message(
441 self.request.POST.get('test_text', ''),
440 self.request.POST.get('test_text', ''),
442 'repo_group/test_repo1', error_container=error_container)
441 'repo_group/test_repo1', error_container=error_container)
443 if error_container:
442 if error_container:
444 def converter(inp):
443 def converter(inp):
445 return h.html_escape(inp)
444 return h.html_escape(inp)
446
445
447 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
446 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
448
447
449 return urlified_commit
448 return urlified_commit
450
449
451 @LoginRequired()
450 @LoginRequired()
452 @HasPermissionAllDecorator('hg.admin')
451 @HasPermissionAllDecorator('hg.admin')
453 @CSRFRequired()
452 @CSRFRequired()
454 def settings_issuetracker_update(self):
453 def settings_issuetracker_update(self):
455 _ = self.request.translate
454 _ = self.request.translate
456 self.load_default_context()
455 self.load_default_context()
457 settings_model = IssueTrackerSettingsModel()
456 settings_model = IssueTrackerSettingsModel()
458
457
459 try:
458 try:
460 form = IssueTrackerPatternsForm(self.request.translate)()
459 form = IssueTrackerPatternsForm(self.request.translate)()
461 data = form.to_python(self.request.POST)
460 data = form.to_python(self.request.POST)
462 except formencode.Invalid as errors:
461 except formencode.Invalid as errors:
463 log.exception('Failed to add new pattern')
462 log.exception('Failed to add new pattern')
464 error = errors
463 error = errors
465 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
464 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
466 category='error')
465 category='error')
467 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
466 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
468
467
469 if data:
468 if data:
470 for uid in data.get('delete_patterns', []):
469 for uid in data.get('delete_patterns', []):
471 settings_model.delete_entries(uid)
470 settings_model.delete_entries(uid)
472
471
473 for pattern in data.get('patterns', []):
472 for pattern in data.get('patterns', []):
474 for setting, value, type_ in pattern:
473 for setting, value, type_ in pattern:
475 sett = settings_model.create_or_update_setting(
474 sett = settings_model.create_or_update_setting(
476 setting, value, type_)
475 setting, value, type_)
477 Session().add(sett)
476 Session().add(sett)
478
477
479 Session().commit()
478 Session().commit()
480
479
481 SettingsModel().invalidate_settings_cache()
480 SettingsModel().invalidate_settings_cache()
482 h.flash(_('Updated issue tracker entries'), category='success')
481 h.flash(_('Updated issue tracker entries'), category='success')
483 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
482 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
484
483
485 @LoginRequired()
484 @LoginRequired()
486 @HasPermissionAllDecorator('hg.admin')
485 @HasPermissionAllDecorator('hg.admin')
487 @CSRFRequired()
486 @CSRFRequired()
488 def settings_issuetracker_delete(self):
487 def settings_issuetracker_delete(self):
489 _ = self.request.translate
488 _ = self.request.translate
490 self.load_default_context()
489 self.load_default_context()
491 uid = self.request.POST.get('uid')
490 uid = self.request.POST.get('uid')
492 try:
491 try:
493 IssueTrackerSettingsModel().delete_entries(uid)
492 IssueTrackerSettingsModel().delete_entries(uid)
494 except Exception:
493 except Exception:
495 log.exception('Failed to delete issue tracker setting %s', uid)
494 log.exception('Failed to delete issue tracker setting %s', uid)
496 raise HTTPNotFound()
495 raise HTTPNotFound()
497
496
498 SettingsModel().invalidate_settings_cache()
497 SettingsModel().invalidate_settings_cache()
499 h.flash(_('Removed issue tracker entry.'), category='success')
498 h.flash(_('Removed issue tracker entry.'), category='success')
500
499
501 return {'deleted': uid}
500 return {'deleted': uid}
502
501
503 @LoginRequired()
502 @LoginRequired()
504 @HasPermissionAllDecorator('hg.admin')
503 @HasPermissionAllDecorator('hg.admin')
505 def settings_email(self):
504 def settings_email(self):
506 c = self.load_default_context()
505 c = self.load_default_context()
507 c.active = 'email'
506 c.active = 'email'
508 c.rhodecode_ini = rhodecode.CONFIG
507 c.rhodecode_ini = rhodecode.CONFIG
509
508
510 data = render('rhodecode:templates/admin/settings/settings.mako',
509 data = render('rhodecode:templates/admin/settings/settings.mako',
511 self._get_template_context(c), self.request)
510 self._get_template_context(c), self.request)
512 html = formencode.htmlfill.render(
511 html = formencode.htmlfill.render(
513 data,
512 data,
514 defaults=self._form_defaults(),
513 defaults=self._form_defaults(),
515 encoding="UTF-8",
514 encoding="UTF-8",
516 force_defaults=False
515 force_defaults=False
517 )
516 )
518 return Response(html)
517 return Response(html)
519
518
520 @LoginRequired()
519 @LoginRequired()
521 @HasPermissionAllDecorator('hg.admin')
520 @HasPermissionAllDecorator('hg.admin')
522 @CSRFRequired()
521 @CSRFRequired()
523 def settings_email_update(self):
522 def settings_email_update(self):
524 _ = self.request.translate
523 _ = self.request.translate
525 c = self.load_default_context()
524 c = self.load_default_context()
526 c.active = 'email'
525 c.active = 'email'
527
526
528 test_email = self.request.POST.get('test_email')
527 test_email = self.request.POST.get('test_email')
529
528
530 if not test_email:
529 if not test_email:
531 h.flash(_('Please enter email address'), category='error')
530 h.flash(_('Please enter email address'), category='error')
532 raise HTTPFound(h.route_path('admin_settings_email'))
531 raise HTTPFound(h.route_path('admin_settings_email'))
533
532
534 email_kwargs = {
533 email_kwargs = {
535 'date': datetime.datetime.now(),
534 'date': datetime.datetime.now(),
536 'user': self._rhodecode_db_user
535 'user': self._rhodecode_db_user
537 }
536 }
538
537
539 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
538 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
540 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
539 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
541
540
542 recipients = [test_email] if test_email else None
541 recipients = [test_email] if test_email else None
543
542
544 run_task(tasks.send_email, recipients, subject,
543 run_task(tasks.send_email, recipients, subject,
545 email_body_plaintext, email_body)
544 email_body_plaintext, email_body)
546
545
547 h.flash(_('Send email task created'), category='success')
546 h.flash(_('Send email task created'), category='success')
548 raise HTTPFound(h.route_path('admin_settings_email'))
547 raise HTTPFound(h.route_path('admin_settings_email'))
549
548
550 @LoginRequired()
549 @LoginRequired()
551 @HasPermissionAllDecorator('hg.admin')
550 @HasPermissionAllDecorator('hg.admin')
552 def settings_hooks(self):
551 def settings_hooks(self):
553 c = self.load_default_context()
552 c = self.load_default_context()
554 c.active = 'hooks'
553 c.active = 'hooks'
555
554
556 model = SettingsModel()
555 model = SettingsModel()
557 c.hooks = model.get_builtin_hooks()
556 c.hooks = model.get_builtin_hooks()
558 c.custom_hooks = model.get_custom_hooks()
557 c.custom_hooks = model.get_custom_hooks()
559
558
560 data = render('rhodecode:templates/admin/settings/settings.mako',
559 data = render('rhodecode:templates/admin/settings/settings.mako',
561 self._get_template_context(c), self.request)
560 self._get_template_context(c), self.request)
562 html = formencode.htmlfill.render(
561 html = formencode.htmlfill.render(
563 data,
562 data,
564 defaults=self._form_defaults(),
563 defaults=self._form_defaults(),
565 encoding="UTF-8",
564 encoding="UTF-8",
566 force_defaults=False
565 force_defaults=False
567 )
566 )
568 return Response(html)
567 return Response(html)
569
568
570 @LoginRequired()
569 @LoginRequired()
571 @HasPermissionAllDecorator('hg.admin')
570 @HasPermissionAllDecorator('hg.admin')
572 @CSRFRequired()
571 @CSRFRequired()
573 def settings_hooks_update(self):
572 def settings_hooks_update(self):
574 _ = self.request.translate
573 _ = self.request.translate
575 c = self.load_default_context()
574 c = self.load_default_context()
576 c.active = 'hooks'
575 c.active = 'hooks'
577 if c.visual.allow_custom_hooks_settings:
576 if c.visual.allow_custom_hooks_settings:
578 ui_key = self.request.POST.get('new_hook_ui_key')
577 ui_key = self.request.POST.get('new_hook_ui_key')
579 ui_value = self.request.POST.get('new_hook_ui_value')
578 ui_value = self.request.POST.get('new_hook_ui_value')
580
579
581 hook_id = self.request.POST.get('hook_id')
580 hook_id = self.request.POST.get('hook_id')
582 new_hook = False
581 new_hook = False
583
582
584 model = SettingsModel()
583 model = SettingsModel()
585 try:
584 try:
586 if ui_value and ui_key:
585 if ui_value and ui_key:
587 model.create_or_update_hook(ui_key, ui_value)
586 model.create_or_update_hook(ui_key, ui_value)
588 h.flash(_('Added new hook'), category='success')
587 h.flash(_('Added new hook'), category='success')
589 new_hook = True
588 new_hook = True
590 elif hook_id:
589 elif hook_id:
591 RhodeCodeUi.delete(hook_id)
590 RhodeCodeUi.delete(hook_id)
592 Session().commit()
591 Session().commit()
593
592
594 # check for edits
593 # check for edits
595 update = False
594 update = False
596 _d = self.request.POST.dict_of_lists()
595 _d = self.request.POST.dict_of_lists()
597 for k, v in zip(_d.get('hook_ui_key', []),
596 for k, v in zip(_d.get('hook_ui_key', []),
598 _d.get('hook_ui_value_new', [])):
597 _d.get('hook_ui_value_new', [])):
599 model.create_or_update_hook(k, v)
598 model.create_or_update_hook(k, v)
600 update = True
599 update = True
601
600
602 if update and not new_hook:
601 if update and not new_hook:
603 h.flash(_('Updated hooks'), category='success')
602 h.flash(_('Updated hooks'), category='success')
604 Session().commit()
603 Session().commit()
605 except Exception:
604 except Exception:
606 log.exception("Exception during hook creation")
605 log.exception("Exception during hook creation")
607 h.flash(_('Error occurred during hook creation'),
606 h.flash(_('Error occurred during hook creation'),
608 category='error')
607 category='error')
609
608
610 raise HTTPFound(h.route_path('admin_settings_hooks'))
609 raise HTTPFound(h.route_path('admin_settings_hooks'))
611
610
612 @LoginRequired()
611 @LoginRequired()
613 @HasPermissionAllDecorator('hg.admin')
612 @HasPermissionAllDecorator('hg.admin')
614 def settings_search(self):
613 def settings_search(self):
615 c = self.load_default_context()
614 c = self.load_default_context()
616 c.active = 'search'
615 c.active = 'search'
617
616
618 c.searcher = searcher_from_config(self.request.registry.settings)
617 c.searcher = searcher_from_config(self.request.registry.settings)
619 c.statistics = c.searcher.statistics(self.request.translate)
618 c.statistics = c.searcher.statistics(self.request.translate)
620
619
621 return self._get_template_context(c)
620 return self._get_template_context(c)
622
621
623 @LoginRequired()
622 @LoginRequired()
624 @HasPermissionAllDecorator('hg.admin')
623 @HasPermissionAllDecorator('hg.admin')
625 def settings_automation(self):
624 def settings_automation(self):
626 c = self.load_default_context()
625 c = self.load_default_context()
627 c.active = 'automation'
626 c.active = 'automation'
628
627
629 return self._get_template_context(c)
628 return self._get_template_context(c)
630
629
631 @LoginRequired()
630 @LoginRequired()
632 @HasPermissionAllDecorator('hg.admin')
631 @HasPermissionAllDecorator('hg.admin')
633 def settings_labs(self):
632 def settings_labs(self):
634 c = self.load_default_context()
633 c = self.load_default_context()
635 if not c.labs_active:
634 if not c.labs_active:
636 raise HTTPFound(h.route_path('admin_settings'))
635 raise HTTPFound(h.route_path('admin_settings'))
637
636
638 c.active = 'labs'
637 c.active = 'labs'
639 c.lab_settings = _LAB_SETTINGS
638 c.lab_settings = _LAB_SETTINGS
640
639
641 data = render('rhodecode:templates/admin/settings/settings.mako',
640 data = render('rhodecode:templates/admin/settings/settings.mako',
642 self._get_template_context(c), self.request)
641 self._get_template_context(c), self.request)
643 html = formencode.htmlfill.render(
642 html = formencode.htmlfill.render(
644 data,
643 data,
645 defaults=self._form_defaults(),
644 defaults=self._form_defaults(),
646 encoding="UTF-8",
645 encoding="UTF-8",
647 force_defaults=False
646 force_defaults=False
648 )
647 )
649 return Response(html)
648 return Response(html)
650
649
651 @LoginRequired()
650 @LoginRequired()
652 @HasPermissionAllDecorator('hg.admin')
651 @HasPermissionAllDecorator('hg.admin')
653 @CSRFRequired()
652 @CSRFRequired()
654 def settings_labs_update(self):
653 def settings_labs_update(self):
655 _ = self.request.translate
654 _ = self.request.translate
656 c = self.load_default_context()
655 c = self.load_default_context()
657 c.active = 'labs'
656 c.active = 'labs'
658
657
659 application_form = LabsSettingsForm(self.request.translate)()
658 application_form = LabsSettingsForm(self.request.translate)()
660 try:
659 try:
661 form_result = application_form.to_python(dict(self.request.POST))
660 form_result = application_form.to_python(dict(self.request.POST))
662 except formencode.Invalid as errors:
661 except formencode.Invalid as errors:
663 h.flash(
662 h.flash(
664 _("Some form inputs contain invalid data."),
663 _("Some form inputs contain invalid data."),
665 category='error')
664 category='error')
666 data = render('rhodecode:templates/admin/settings/settings.mako',
665 data = render('rhodecode:templates/admin/settings/settings.mako',
667 self._get_template_context(c), self.request)
666 self._get_template_context(c), self.request)
668 html = formencode.htmlfill.render(
667 html = formencode.htmlfill.render(
669 data,
668 data,
670 defaults=errors.value,
669 defaults=errors.value,
671 errors=errors.unpack_errors() or {},
670 errors=errors.unpack_errors() or {},
672 prefix_error=False,
671 prefix_error=False,
673 encoding="UTF-8",
672 encoding="UTF-8",
674 force_defaults=False
673 force_defaults=False
675 )
674 )
676 return Response(html)
675 return Response(html)
677
676
678 try:
677 try:
679 session = Session()
678 session = Session()
680 for setting in _LAB_SETTINGS:
679 for setting in _LAB_SETTINGS:
681 setting_name = setting.key[len('rhodecode_'):]
680 setting_name = setting.key[len('rhodecode_'):]
682 sett = SettingsModel().create_or_update_setting(
681 sett = SettingsModel().create_or_update_setting(
683 setting_name, form_result[setting.key], setting.type)
682 setting_name, form_result[setting.key], setting.type)
684 session.add(sett)
683 session.add(sett)
685
684
686 except Exception:
685 except Exception:
687 log.exception('Exception while updating lab settings')
686 log.exception('Exception while updating lab settings')
688 h.flash(_('Error occurred during updating labs settings'),
687 h.flash(_('Error occurred during updating labs settings'),
689 category='error')
688 category='error')
690 else:
689 else:
691 Session().commit()
690 Session().commit()
692 SettingsModel().invalidate_settings_cache()
691 SettingsModel().invalidate_settings_cache()
693 h.flash(_('Updated Labs settings'), category='success')
692 h.flash(_('Updated Labs settings'), category='success')
694 raise HTTPFound(h.route_path('admin_settings_labs'))
693 raise HTTPFound(h.route_path('admin_settings_labs'))
695
694
696 data = render('rhodecode:templates/admin/settings/settings.mako',
695 data = render('rhodecode:templates/admin/settings/settings.mako',
697 self._get_template_context(c), self.request)
696 self._get_template_context(c), self.request)
698 html = formencode.htmlfill.render(
697 html = formencode.htmlfill.render(
699 data,
698 data,
700 defaults=self._form_defaults(),
699 defaults=self._form_defaults(),
701 encoding="UTF-8",
700 encoding="UTF-8",
702 force_defaults=False
701 force_defaults=False
703 )
702 )
704 return Response(html)
703 return Response(html)
705
704
706
705
707 # :param key: name of the setting including the 'rhodecode_' prefix
706 # :param key: name of the setting including the 'rhodecode_' prefix
708 # :param type: the RhodeCodeSetting type to use.
707 # :param type: the RhodeCodeSetting type to use.
709 # :param group: the i18ned group in which we should dispaly this setting
708 # :param group: the i18ned group in which we should dispaly this setting
710 # :param label: the i18ned label we should display for this setting
709 # :param label: the i18ned label we should display for this setting
711 # :param help: the i18ned help we should dispaly for this setting
710 # :param help: the i18ned help we should dispaly for this setting
712 LabSetting = collections.namedtuple(
711 LabSetting = collections.namedtuple(
713 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
712 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
714
713
715
714
716 # This list has to be kept in sync with the form
715 # This list has to be kept in sync with the form
717 # rhodecode.model.forms.LabsSettingsForm.
716 # rhodecode.model.forms.LabsSettingsForm.
718 _LAB_SETTINGS = [
717 _LAB_SETTINGS = [
719
718
720 ]
719 ]
@@ -1,56 +1,56 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23
23
24
24
25 from rhodecode.apps._base import BaseAppView
25 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
26 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
27 from rhodecode.lib.auth import (
27 from rhodecode.lib.auth import (
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 class AdminSvnConfigView(BaseAppView):
33 class AdminSvnConfigView(BaseAppView):
34
34
35 @LoginRequired()
35 @LoginRequired()
36 @HasPermissionAllDecorator('hg.admin')
36 @HasPermissionAllDecorator('hg.admin')
37 @CSRFRequired()
37 @CSRFRequired()
38 def vcs_svn_generate_config(self):
38 def vcs_svn_generate_config(self):
39 _ = self.request.translate
39 _ = self.request.translate
40 try:
40 try:
41 file_path = generate_mod_dav_svn_config(self.request.registry)
41 file_path = generate_mod_dav_svn_config(self.request.registry)
42 msg = {
42 msg = {
43 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
43 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
44 'level': 'success',
44 'level': 'success',
45 }
45 }
46 except Exception:
46 except Exception:
47 log.exception(
47 log.exception(
48 'Exception while generating the Apache '
48 'Exception while generating the Apache '
49 'configuration for Subversion.')
49 'configuration for Subversion.')
50 msg = {
50 msg = {
51 'message': _('Failed to generate the Apache configuration for Subversion.'),
51 'message': _('Failed to generate the Apache configuration for Subversion.'),
52 'level': 'error',
52 'level': 'error',
53 }
53 }
54
54
55 data = {'message': msg}
55 data = {'message': msg}
56 return data
56 return data
@@ -1,234 +1,234 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import urllib.request, urllib.error, urllib.parse
22 import urllib.request, urllib.error, urllib.parse
23 import os
23 import os
24
24
25 import rhodecode
25 import rhodecode
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base.navigation import navigation_list
27 from rhodecode.apps._base.navigation import navigation_list
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.lib.utils2 import str2bool
30 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib import system_info
31 from rhodecode.lib import system_info
32 from rhodecode.model.update import UpdateModel
32 from rhodecode.model.update import UpdateModel
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 class AdminSystemInfoSettingsView(BaseAppView):
37 class AdminSystemInfoSettingsView(BaseAppView):
38 def load_default_context(self):
38 def load_default_context(self):
39 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
40 return c
40 return c
41
41
42 def get_env_data(self):
42 def get_env_data(self):
43 black_list = [
43 black_list = [
44 'NIX_LDFLAGS',
44 'NIX_LDFLAGS',
45 'NIX_CFLAGS_COMPILE',
45 'NIX_CFLAGS_COMPILE',
46 'propagatedBuildInputs',
46 'propagatedBuildInputs',
47 'propagatedNativeBuildInputs',
47 'propagatedNativeBuildInputs',
48 'postInstall',
48 'postInstall',
49 'buildInputs',
49 'buildInputs',
50 'buildPhase',
50 'buildPhase',
51 'preShellHook',
51 'preShellHook',
52 'preShellHook',
52 'preShellHook',
53 'preCheck',
53 'preCheck',
54 'preBuild',
54 'preBuild',
55 'postShellHook',
55 'postShellHook',
56 'postFixup',
56 'postFixup',
57 'postCheck',
57 'postCheck',
58 'nativeBuildInputs',
58 'nativeBuildInputs',
59 'installPhase',
59 'installPhase',
60 'installCheckPhase',
60 'installCheckPhase',
61 'checkPhase',
61 'checkPhase',
62 'configurePhase',
62 'configurePhase',
63 'shellHook'
63 'shellHook'
64 ]
64 ]
65 secret_list = [
65 secret_list = [
66 'RHODECODE_USER_PASS'
66 'RHODECODE_USER_PASS'
67 ]
67 ]
68
68
69 for k, v in sorted(os.environ.items()):
69 for k, v in sorted(os.environ.items()):
70 if k in black_list:
70 if k in black_list:
71 continue
71 continue
72 if k in secret_list:
72 if k in secret_list:
73 v = '*****'
73 v = '*****'
74 yield k, v
74 yield k, v
75
75
76 @LoginRequired()
76 @LoginRequired()
77 @HasPermissionAllDecorator('hg.admin')
77 @HasPermissionAllDecorator('hg.admin')
78 def settings_system_info(self):
78 def settings_system_info(self):
79 _ = self.request.translate
79 _ = self.request.translate
80 c = self.load_default_context()
80 c = self.load_default_context()
81
81
82 c.active = 'system'
82 c.active = 'system'
83 c.navlist = navigation_list(self.request)
83 c.navlist = navigation_list(self.request)
84
84
85 # TODO(marcink), figure out how to allow only selected users to do this
85 # TODO(marcink), figure out how to allow only selected users to do this
86 c.allowed_to_snapshot = self._rhodecode_user.admin
86 c.allowed_to_snapshot = self._rhodecode_user.admin
87
87
88 snapshot = str2bool(self.request.params.get('snapshot'))
88 snapshot = str2bool(self.request.params.get('snapshot'))
89
89
90 c.rhodecode_update_url = UpdateModel().get_update_url()
90 c.rhodecode_update_url = UpdateModel().get_update_url()
91 c.env_data = self.get_env_data()
91 c.env_data = self.get_env_data()
92 server_info = system_info.get_system_info(self.request.environ)
92 server_info = system_info.get_system_info(self.request.environ)
93
93
94 for key, val in server_info.items():
94 for key, val in server_info.items():
95 setattr(c, key, val)
95 setattr(c, key, val)
96
96
97 def val(name, subkey='human_value'):
97 def val(name, subkey='human_value'):
98 return server_info[name][subkey]
98 return server_info[name][subkey]
99
99
100 def state(name):
100 def state(name):
101 return server_info[name]['state']
101 return server_info[name]['state']
102
102
103 def val2(name):
103 def val2(name):
104 val = server_info[name]['human_value']
104 val = server_info[name]['human_value']
105 state = server_info[name]['state']
105 state = server_info[name]['state']
106 return val, state
106 return val, state
107
107
108 update_info_msg = _('Note: please make sure this server can '
108 update_info_msg = _('Note: please make sure this server can '
109 'access `${url}` for the update link to work',
109 'access `${url}` for the update link to work',
110 mapping=dict(url=c.rhodecode_update_url))
110 mapping=dict(url=c.rhodecode_update_url))
111 version = UpdateModel().get_stored_version()
111 version = UpdateModel().get_stored_version()
112 is_outdated = UpdateModel().is_outdated(
112 is_outdated = UpdateModel().is_outdated(
113 rhodecode.__version__, version)
113 rhodecode.__version__, version)
114 update_state = {
114 update_state = {
115 'type': 'warning',
115 'type': 'warning',
116 'message': 'New version available: {}'.format(version)
116 'message': 'New version available: {}'.format(version)
117 } \
117 } \
118 if is_outdated else {}
118 if is_outdated else {}
119 c.data_items = [
119 c.data_items = [
120 # update info
120 # update info
121 (_('Update info'), h.literal(
121 (_('Update info'), h.literal(
122 '<span class="link" id="check_for_update" >%s.</span>' % (
122 '<span class="link" id="check_for_update" >%s.</span>' % (
123 _('Check for updates')) +
123 _('Check for updates')) +
124 '<br/> <span >%s.</span>' % (update_info_msg)
124 '<br/> <span >%s.</span>' % (update_info_msg)
125 ), ''),
125 ), ''),
126
126
127 # RhodeCode specific
127 # RhodeCode specific
128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
128 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
129 (_('Latest version'), version, update_state),
129 (_('Latest version'), version, update_state),
130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
130 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
131 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
132 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
133 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
134 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
135 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
136 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
137 ('', '', ''), # spacer
137 ('', '', ''), # spacer
138
138
139 # Database
139 # Database
140 (_('Database'), val('database')['url'], state('database')),
140 (_('Database'), val('database')['url'], state('database')),
141 (_('Database version'), val('database')['version'], state('database')),
141 (_('Database version'), val('database')['version'], state('database')),
142 ('', '', ''), # spacer
142 ('', '', ''), # spacer
143
143
144 # Platform/Python
144 # Platform/Python
145 (_('Platform'), val('platform')['name'], state('platform')),
145 (_('Platform'), val('platform')['name'], state('platform')),
146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
146 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
147 (_('Lang'), val('locale'), state('locale')),
147 (_('Lang'), val('locale'), state('locale')),
148 (_('Python version'), val('python')['version'], state('python')),
148 (_('Python version'), val('python')['version'], state('python')),
149 (_('Python path'), val('python')['executable'], state('python')),
149 (_('Python path'), val('python')['executable'], state('python')),
150 ('', '', ''), # spacer
150 ('', '', ''), # spacer
151
151
152 # Systems stats
152 # Systems stats
153 (_('CPU'), val('cpu')['text'], state('cpu')),
153 (_('CPU'), val('cpu')['text'], state('cpu')),
154 (_('Load'), val('load')['text'], state('load')),
154 (_('Load'), val('load')['text'], state('load')),
155 (_('Memory'), val('memory')['text'], state('memory')),
155 (_('Memory'), val('memory')['text'], state('memory')),
156 (_('Uptime'), val('uptime')['text'], state('uptime')),
156 (_('Uptime'), val('uptime')['text'], state('uptime')),
157 ('', '', ''), # spacer
157 ('', '', ''), # spacer
158
158
159 # ulimit
159 # ulimit
160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
160 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
161
161
162 # Repo storage
162 # Repo storage
163 (_('Storage location'), val('storage')['path'], state('storage')),
163 (_('Storage location'), val('storage')['path'], state('storage')),
164 (_('Storage info'), val('storage')['text'], state('storage')),
164 (_('Storage info'), val('storage')['text'], state('storage')),
165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
165 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
166
166
167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
167 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
168 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
169
169
170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
170 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
171 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
172
172
173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
173 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
174 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
175
175
176 (_('Search info'), val('search')['text'], state('search')),
176 (_('Search info'), val('search')['text'], state('search')),
177 (_('Search location'), val('search')['location'], state('search')),
177 (_('Search location'), val('search')['location'], state('search')),
178 ('', '', ''), # spacer
178 ('', '', ''), # spacer
179
179
180 # VCS specific
180 # VCS specific
181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
181 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
182 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
183 (_('GIT'), val('git'), state('git')),
183 (_('GIT'), val('git'), state('git')),
184 (_('HG'), val('hg'), state('hg')),
184 (_('HG'), val('hg'), state('hg')),
185 (_('SVN'), val('svn'), state('svn')),
185 (_('SVN'), val('svn'), state('svn')),
186
186
187 ]
187 ]
188
188
189 c.vcsserver_data_items = [
189 c.vcsserver_data_items = [
190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
190 (k, v) for k,v in (val('vcs_server_config') or {}).items()
191 ]
191 ]
192
192
193 if snapshot:
193 if snapshot:
194 if c.allowed_to_snapshot:
194 if c.allowed_to_snapshot:
195 c.data_items.pop(0) # remove server info
195 c.data_items.pop(0) # remove server info
196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
196 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
197 else:
197 else:
198 h.flash('You are not allowed to do this', category='warning')
198 h.flash('You are not allowed to do this', category='warning')
199 return self._get_template_context(c)
199 return self._get_template_context(c)
200
200
201 @LoginRequired()
201 @LoginRequired()
202 @HasPermissionAllDecorator('hg.admin')
202 @HasPermissionAllDecorator('hg.admin')
203 def settings_system_info_check_update(self):
203 def settings_system_info_check_update(self):
204 _ = self.request.translate
204 _ = self.request.translate
205 c = self.load_default_context()
205 c = self.load_default_context()
206
206
207 update_url = UpdateModel().get_update_url()
207 update_url = UpdateModel().get_update_url()
208
208
209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
209 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
210 try:
210 try:
211 data = UpdateModel().get_update_data(update_url)
211 data = UpdateModel().get_update_data(update_url)
212 except urllib.error.URLError as e:
212 except urllib.error.URLError as e:
213 log.exception("Exception contacting upgrade server")
213 log.exception("Exception contacting upgrade server")
214 self.request.override_renderer = 'string'
214 self.request.override_renderer = 'string'
215 return _err('Failed to contact upgrade server: %r' % e)
215 return _err('Failed to contact upgrade server: %r' % e)
216 except ValueError as e:
216 except ValueError as e:
217 log.exception("Bad data sent from update server")
217 log.exception("Bad data sent from update server")
218 self.request.override_renderer = 'string'
218 self.request.override_renderer = 'string'
219 return _err('Bad data sent from update server')
219 return _err('Bad data sent from update server')
220
220
221 latest = data['versions'][0]
221 latest = data['versions'][0]
222
222
223 c.update_url = update_url
223 c.update_url = update_url
224 c.latest_data = latest
224 c.latest_data = latest
225 c.latest_ver = latest['version']
225 c.latest_ver = latest['version']
226 c.cur_ver = rhodecode.__version__
226 c.cur_ver = rhodecode.__version__
227 c.should_upgrade = False
227 c.should_upgrade = False
228
228
229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
229 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
230 if is_oudated:
230 if is_oudated:
231 c.should_upgrade = True
231 c.should_upgrade = True
232 c.important_notices = latest['general']
232 c.important_notices = latest['general']
233 UpdateModel().store_version(latest['version'])
233 UpdateModel().store_version(latest['version'])
234 return self._get_template_context(c)
234 return self._get_template_context(c)
@@ -1,253 +1,253 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22
22
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27
27
28 from pyramid.response import Response
28 from pyramid.response import Response
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30
30
31 from rhodecode import events
31 from rhodecode import events
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
35 from rhodecode.lib import helpers as h, audit_logger
35 from rhodecode.lib import helpers as h, audit_logger
36 from rhodecode.lib.utils2 import safe_unicode
36 from rhodecode.lib.utils2 import safe_unicode
37
37
38 from rhodecode.model.forms import UserGroupForm
38 from rhodecode.model.forms import UserGroupForm
39 from rhodecode.model.permission import PermissionModel
39 from rhodecode.model.permission import PermissionModel
40 from rhodecode.model.scm import UserGroupList
40 from rhodecode.model.scm import UserGroupList
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.user_group import UserGroupModel
44 from rhodecode.model.user_group import UserGroupModel
45 from rhodecode.model.db import true
45 from rhodecode.model.db import true
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class AdminUserGroupsView(BaseAppView, DataGridAppView):
50 class AdminUserGroupsView(BaseAppView, DataGridAppView):
51
51
52 def load_default_context(self):
52 def load_default_context(self):
53 c = self._get_local_tmpl_context()
53 c = self._get_local_tmpl_context()
54 PermissionModel().set_global_permission_choices(
54 PermissionModel().set_global_permission_choices(
55 c, gettext_translator=self.request.translate)
55 c, gettext_translator=self.request.translate)
56 return c
56 return c
57
57
58 # permission check in data loading of
58 # permission check in data loading of
59 # `user_groups_list_data` via UserGroupList
59 # `user_groups_list_data` via UserGroupList
60 @LoginRequired()
60 @LoginRequired()
61 @NotAnonymous()
61 @NotAnonymous()
62 def user_groups_list(self):
62 def user_groups_list(self):
63 c = self.load_default_context()
63 c = self.load_default_context()
64 return self._get_template_context(c)
64 return self._get_template_context(c)
65
65
66 # permission check inside
66 # permission check inside
67 @LoginRequired()
67 @LoginRequired()
68 @NotAnonymous()
68 @NotAnonymous()
69 def user_groups_list_data(self):
69 def user_groups_list_data(self):
70 self.load_default_context()
70 self.load_default_context()
71 column_map = {
71 column_map = {
72 'active': 'users_group_active',
72 'active': 'users_group_active',
73 'description': 'user_group_description',
73 'description': 'user_group_description',
74 'members': 'members_total',
74 'members': 'members_total',
75 'owner': 'user_username',
75 'owner': 'user_username',
76 'sync': 'group_data'
76 'sync': 'group_data'
77 }
77 }
78 draw, start, limit = self._extract_chunk(self.request)
78 draw, start, limit = self._extract_chunk(self.request)
79 search_q, order_by, order_dir = self._extract_ordering(
79 search_q, order_by, order_dir = self._extract_ordering(
80 self.request, column_map=column_map)
80 self.request, column_map=column_map)
81
81
82 _render = self.request.get_partial_renderer(
82 _render = self.request.get_partial_renderer(
83 'rhodecode:templates/data_table/_dt_elements.mako')
83 'rhodecode:templates/data_table/_dt_elements.mako')
84
84
85 def user_group_name(user_group_name):
85 def user_group_name(user_group_name):
86 return _render("user_group_name", user_group_name)
86 return _render("user_group_name", user_group_name)
87
87
88 def user_group_actions(user_group_id, user_group_name):
88 def user_group_actions(user_group_id, user_group_name):
89 return _render("user_group_actions", user_group_id, user_group_name)
89 return _render("user_group_actions", user_group_id, user_group_name)
90
90
91 def user_profile(username):
91 def user_profile(username):
92 return _render('user_profile', username)
92 return _render('user_profile', username)
93
93
94 _perms = ['usergroup.admin']
94 _perms = ['usergroup.admin']
95 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
95 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
96
96
97 user_groups_data_total_count = UserGroup.query()\
97 user_groups_data_total_count = UserGroup.query()\
98 .filter(or_(
98 .filter(or_(
99 # generate multiple IN to fix limitation problems
99 # generate multiple IN to fix limitation problems
100 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
100 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
101 ))\
101 ))\
102 .count()
102 .count()
103
103
104 user_groups_data_total_inactive_count = UserGroup.query()\
104 user_groups_data_total_inactive_count = UserGroup.query()\
105 .filter(or_(
105 .filter(or_(
106 # generate multiple IN to fix limitation problems
106 # generate multiple IN to fix limitation problems
107 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
107 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
108 ))\
108 ))\
109 .filter(UserGroup.users_group_active != true()).count()
109 .filter(UserGroup.users_group_active != true()).count()
110
110
111 member_count = count(UserGroupMember.user_id)
111 member_count = count(UserGroupMember.user_id)
112 base_q = Session.query(
112 base_q = Session.query(
113 UserGroup.users_group_name,
113 UserGroup.users_group_name,
114 UserGroup.user_group_description,
114 UserGroup.user_group_description,
115 UserGroup.users_group_active,
115 UserGroup.users_group_active,
116 UserGroup.users_group_id,
116 UserGroup.users_group_id,
117 UserGroup.group_data,
117 UserGroup.group_data,
118 User,
118 User,
119 member_count.label('member_count')
119 member_count.label('member_count')
120 ) \
120 ) \
121 .filter(or_(
121 .filter(or_(
122 # generate multiple IN to fix limitation problems
122 # generate multiple IN to fix limitation problems
123 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
123 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
124 )) \
124 )) \
125 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
125 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
126 .join(User, User.user_id == UserGroup.user_id) \
126 .join(User, User.user_id == UserGroup.user_id) \
127 .group_by(UserGroup, User)
127 .group_by(UserGroup, User)
128
128
129 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
129 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
130
130
131 if search_q:
131 if search_q:
132 like_expression = u'%{}%'.format(safe_unicode(search_q))
132 like_expression = u'%{}%'.format(safe_unicode(search_q))
133 base_q = base_q.filter(or_(
133 base_q = base_q.filter(or_(
134 UserGroup.users_group_name.ilike(like_expression),
134 UserGroup.users_group_name.ilike(like_expression),
135 ))
135 ))
136 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
136 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
137
137
138 user_groups_data_total_filtered_count = base_q.count()
138 user_groups_data_total_filtered_count = base_q.count()
139 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
139 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
140
140
141 sort_defined = False
141 sort_defined = False
142 if order_by == 'members_total':
142 if order_by == 'members_total':
143 sort_col = member_count
143 sort_col = member_count
144 sort_defined = True
144 sort_defined = True
145 elif order_by == 'user_username':
145 elif order_by == 'user_username':
146 sort_col = User.username
146 sort_col = User.username
147 else:
147 else:
148 sort_col = getattr(UserGroup, order_by, None)
148 sort_col = getattr(UserGroup, order_by, None)
149
149
150 if sort_defined or sort_col:
150 if sort_defined or sort_col:
151 if order_dir == 'asc':
151 if order_dir == 'asc':
152 sort_col = sort_col.asc()
152 sort_col = sort_col.asc()
153 else:
153 else:
154 sort_col = sort_col.desc()
154 sort_col = sort_col.desc()
155
155
156 base_q = base_q.order_by(sort_col)
156 base_q = base_q.order_by(sort_col)
157 base_q = base_q.offset(start).limit(limit)
157 base_q = base_q.offset(start).limit(limit)
158
158
159 # authenticated access to user groups
159 # authenticated access to user groups
160 auth_user_group_list = base_q.all()
160 auth_user_group_list = base_q.all()
161
161
162 user_groups_data = []
162 user_groups_data = []
163 for user_gr in auth_user_group_list:
163 for user_gr in auth_user_group_list:
164 row = {
164 row = {
165 "users_group_name": user_group_name(user_gr.users_group_name),
165 "users_group_name": user_group_name(user_gr.users_group_name),
166 "description": h.escape(user_gr.user_group_description),
166 "description": h.escape(user_gr.user_group_description),
167 "members": user_gr.member_count,
167 "members": user_gr.member_count,
168 # NOTE(marcink): because of advanced query we
168 # NOTE(marcink): because of advanced query we
169 # need to load it like that
169 # need to load it like that
170 "sync": UserGroup._load_sync(
170 "sync": UserGroup._load_sync(
171 UserGroup._load_group_data(user_gr.group_data)),
171 UserGroup._load_group_data(user_gr.group_data)),
172 "active": h.bool2icon(user_gr.users_group_active),
172 "active": h.bool2icon(user_gr.users_group_active),
173 "owner": user_profile(user_gr.User.username),
173 "owner": user_profile(user_gr.User.username),
174 "action": user_group_actions(
174 "action": user_group_actions(
175 user_gr.users_group_id, user_gr.users_group_name)
175 user_gr.users_group_id, user_gr.users_group_name)
176 }
176 }
177 user_groups_data.append(row)
177 user_groups_data.append(row)
178
178
179 data = ({
179 data = ({
180 'draw': draw,
180 'draw': draw,
181 'data': user_groups_data,
181 'data': user_groups_data,
182 'recordsTotal': user_groups_data_total_count,
182 'recordsTotal': user_groups_data_total_count,
183 'recordsTotalInactive': user_groups_data_total_inactive_count,
183 'recordsTotalInactive': user_groups_data_total_inactive_count,
184 'recordsFiltered': user_groups_data_total_filtered_count,
184 'recordsFiltered': user_groups_data_total_filtered_count,
185 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
185 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
186 })
186 })
187
187
188 return data
188 return data
189
189
190 @LoginRequired()
190 @LoginRequired()
191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 def user_groups_new(self):
192 def user_groups_new(self):
193 c = self.load_default_context()
193 c = self.load_default_context()
194 return self._get_template_context(c)
194 return self._get_template_context(c)
195
195
196 @LoginRequired()
196 @LoginRequired()
197 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
197 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
198 @CSRFRequired()
198 @CSRFRequired()
199 def user_groups_create(self):
199 def user_groups_create(self):
200 _ = self.request.translate
200 _ = self.request.translate
201 c = self.load_default_context()
201 c = self.load_default_context()
202 users_group_form = UserGroupForm(self.request.translate)()
202 users_group_form = UserGroupForm(self.request.translate)()
203
203
204 user_group_name = self.request.POST.get('users_group_name')
204 user_group_name = self.request.POST.get('users_group_name')
205 try:
205 try:
206 form_result = users_group_form.to_python(dict(self.request.POST))
206 form_result = users_group_form.to_python(dict(self.request.POST))
207 user_group = UserGroupModel().create(
207 user_group = UserGroupModel().create(
208 name=form_result['users_group_name'],
208 name=form_result['users_group_name'],
209 description=form_result['user_group_description'],
209 description=form_result['user_group_description'],
210 owner=self._rhodecode_user.user_id,
210 owner=self._rhodecode_user.user_id,
211 active=form_result['users_group_active'])
211 active=form_result['users_group_active'])
212 Session().flush()
212 Session().flush()
213 creation_data = user_group.get_api_data()
213 creation_data = user_group.get_api_data()
214 user_group_name = form_result['users_group_name']
214 user_group_name = form_result['users_group_name']
215
215
216 audit_logger.store_web(
216 audit_logger.store_web(
217 'user_group.create', action_data={'data': creation_data},
217 'user_group.create', action_data={'data': creation_data},
218 user=self._rhodecode_user)
218 user=self._rhodecode_user)
219
219
220 user_group_link = h.link_to(
220 user_group_link = h.link_to(
221 h.escape(user_group_name),
221 h.escape(user_group_name),
222 h.route_path(
222 h.route_path(
223 'edit_user_group', user_group_id=user_group.users_group_id))
223 'edit_user_group', user_group_id=user_group.users_group_id))
224 h.flash(h.literal(_('Created user group %(user_group_link)s')
224 h.flash(h.literal(_('Created user group %(user_group_link)s')
225 % {'user_group_link': user_group_link}),
225 % {'user_group_link': user_group_link}),
226 category='success')
226 category='success')
227 Session().commit()
227 Session().commit()
228 user_group_id = user_group.users_group_id
228 user_group_id = user_group.users_group_id
229 except formencode.Invalid as errors:
229 except formencode.Invalid as errors:
230
230
231 data = render(
231 data = render(
232 'rhodecode:templates/admin/user_groups/user_group_add.mako',
232 'rhodecode:templates/admin/user_groups/user_group_add.mako',
233 self._get_template_context(c), self.request)
233 self._get_template_context(c), self.request)
234 html = formencode.htmlfill.render(
234 html = formencode.htmlfill.render(
235 data,
235 data,
236 defaults=errors.value,
236 defaults=errors.value,
237 errors=errors.unpack_errors() or {},
237 errors=errors.unpack_errors() or {},
238 prefix_error=False,
238 prefix_error=False,
239 encoding="UTF-8",
239 encoding="UTF-8",
240 force_defaults=False
240 force_defaults=False
241 )
241 )
242 return Response(html)
242 return Response(html)
243
243
244 except Exception:
244 except Exception:
245 log.exception("Exception creating user group")
245 log.exception("Exception creating user group")
246 h.flash(_('Error occurred during creation of user group %s') \
246 h.flash(_('Error occurred during creation of user group %s') \
247 % user_group_name, category='error')
247 % user_group_name, category='error')
248 raise HTTPFound(h.route_path('user_groups_new'))
248 raise HTTPFound(h.route_path('user_groups_new'))
249
249
250 PermissionModel().trigger_permission_flush()
250 PermissionModel().trigger_permission_flush()
251
251
252 raise HTTPFound(
252 raise HTTPFound(
253 h.route_path('edit_user_group', user_group_id=user_group_id))
253 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1322 +1,1322 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.authentication.plugins import auth_rhodecode
35 from rhodecode.events import trigger
35 from rhodecode.events import trigger
36 from rhodecode.model.db import true, UserNotice
36 from rhodecode.model.db import true, UserNotice
37
37
38 from rhodecode.lib import audit_logger, rc_cache, auth
38 from rhodecode.lib import audit_logger, rc_cache, auth
39 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 UserOwnsArtifactsException, DefaultUserException)
42 UserOwnsArtifactsException, DefaultUserException)
43 from rhodecode.lib import ext_json
43 from rhodecode.lib import ext_json
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 from rhodecode.lib import helpers as h
46 from rhodecode.lib import helpers as h
47 from rhodecode.lib.helpers import SqlPage
47 from rhodecode.lib.helpers import SqlPage
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.forms import (
50 from rhodecode.model.forms import (
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 UserExtraEmailForm, UserExtraIpForm)
52 UserExtraEmailForm, UserExtraIpForm)
53 from rhodecode.model.permission import PermissionModel
53 from rhodecode.model.permission import PermissionModel
54 from rhodecode.model.repo_group import RepoGroupModel
54 from rhodecode.model.repo_group import RepoGroupModel
55 from rhodecode.model.ssh_key import SshKeyModel
55 from rhodecode.model.ssh_key import SshKeyModel
56 from rhodecode.model.user import UserModel
56 from rhodecode.model.user import UserModel
57 from rhodecode.model.user_group import UserGroupModel
57 from rhodecode.model.user_group import UserGroupModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 UserApiKeys, UserSshKeys, RepoGroup)
60 UserApiKeys, UserSshKeys, RepoGroup)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 class AdminUsersView(BaseAppView, DataGridAppView):
66 class AdminUsersView(BaseAppView, DataGridAppView):
67
67
68 def load_default_context(self):
68 def load_default_context(self):
69 c = self._get_local_tmpl_context()
69 c = self._get_local_tmpl_context()
70 return c
70 return c
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @HasPermissionAllDecorator('hg.admin')
73 @HasPermissionAllDecorator('hg.admin')
74 def users_list(self):
74 def users_list(self):
75 c = self.load_default_context()
75 c = self.load_default_context()
76 return self._get_template_context(c)
76 return self._get_template_context(c)
77
77
78 @LoginRequired()
78 @LoginRequired()
79 @HasPermissionAllDecorator('hg.admin')
79 @HasPermissionAllDecorator('hg.admin')
80 def users_list_data(self):
80 def users_list_data(self):
81 self.load_default_context()
81 self.load_default_context()
82 column_map = {
82 column_map = {
83 'first_name': 'name',
83 'first_name': 'name',
84 'last_name': 'lastname',
84 'last_name': 'lastname',
85 }
85 }
86 draw, start, limit = self._extract_chunk(self.request)
86 draw, start, limit = self._extract_chunk(self.request)
87 search_q, order_by, order_dir = self._extract_ordering(
87 search_q, order_by, order_dir = self._extract_ordering(
88 self.request, column_map=column_map)
88 self.request, column_map=column_map)
89 _render = self.request.get_partial_renderer(
89 _render = self.request.get_partial_renderer(
90 'rhodecode:templates/data_table/_dt_elements.mako')
90 'rhodecode:templates/data_table/_dt_elements.mako')
91
91
92 def user_actions(user_id, username):
92 def user_actions(user_id, username):
93 return _render("user_actions", user_id, username)
93 return _render("user_actions", user_id, username)
94
94
95 users_data_total_count = User.query()\
95 users_data_total_count = User.query()\
96 .filter(User.username != User.DEFAULT_USER) \
96 .filter(User.username != User.DEFAULT_USER) \
97 .count()
97 .count()
98
98
99 users_data_total_inactive_count = User.query()\
99 users_data_total_inactive_count = User.query()\
100 .filter(User.username != User.DEFAULT_USER) \
100 .filter(User.username != User.DEFAULT_USER) \
101 .filter(User.active != true())\
101 .filter(User.active != true())\
102 .count()
102 .count()
103
103
104 # json generate
104 # json generate
105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 base_inactive_q = base_q.filter(User.active != true())
106 base_inactive_q = base_q.filter(User.active != true())
107
107
108 if search_q:
108 if search_q:
109 like_expression = '%{}%'.format(safe_unicode(search_q))
109 like_expression = '%{}%'.format(safe_unicode(search_q))
110 base_q = base_q.filter(or_(
110 base_q = base_q.filter(or_(
111 User.username.ilike(like_expression),
111 User.username.ilike(like_expression),
112 User._email.ilike(like_expression),
112 User._email.ilike(like_expression),
113 User.name.ilike(like_expression),
113 User.name.ilike(like_expression),
114 User.lastname.ilike(like_expression),
114 User.lastname.ilike(like_expression),
115 ))
115 ))
116 base_inactive_q = base_q.filter(User.active != true())
116 base_inactive_q = base_q.filter(User.active != true())
117
117
118 users_data_total_filtered_count = base_q.count()
118 users_data_total_filtered_count = base_q.count()
119 users_data_total_filtered_inactive_count = base_inactive_q.count()
119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120
120
121 sort_col = getattr(User, order_by, None)
121 sort_col = getattr(User, order_by, None)
122 if sort_col:
122 if sort_col:
123 if order_dir == 'asc':
123 if order_dir == 'asc':
124 # handle null values properly to order by NULL last
124 # handle null values properly to order by NULL last
125 if order_by in ['last_activity']:
125 if order_by in ['last_activity']:
126 sort_col = coalesce(sort_col, datetime.date.max)
126 sort_col = coalesce(sort_col, datetime.date.max)
127 sort_col = sort_col.asc()
127 sort_col = sort_col.asc()
128 else:
128 else:
129 # handle null values properly to order by NULL last
129 # handle null values properly to order by NULL last
130 if order_by in ['last_activity']:
130 if order_by in ['last_activity']:
131 sort_col = coalesce(sort_col, datetime.date.min)
131 sort_col = coalesce(sort_col, datetime.date.min)
132 sort_col = sort_col.desc()
132 sort_col = sort_col.desc()
133
133
134 base_q = base_q.order_by(sort_col)
134 base_q = base_q.order_by(sort_col)
135 base_q = base_q.offset(start).limit(limit)
135 base_q = base_q.offset(start).limit(limit)
136
136
137 users_list = base_q.all()
137 users_list = base_q.all()
138
138
139 users_data = []
139 users_data = []
140 for user in users_list:
140 for user in users_list:
141 users_data.append({
141 users_data.append({
142 "username": h.gravatar_with_user(self.request, user.username),
142 "username": h.gravatar_with_user(self.request, user.username),
143 "email": user.email,
143 "email": user.email,
144 "first_name": user.first_name,
144 "first_name": user.first_name,
145 "last_name": user.last_name,
145 "last_name": user.last_name,
146 "last_login": h.format_date(user.last_login),
146 "last_login": h.format_date(user.last_login),
147 "last_activity": h.format_date(user.last_activity),
147 "last_activity": h.format_date(user.last_activity),
148 "active": h.bool2icon(user.active),
148 "active": h.bool2icon(user.active),
149 "active_raw": user.active,
149 "active_raw": user.active,
150 "admin": h.bool2icon(user.admin),
150 "admin": h.bool2icon(user.admin),
151 "extern_type": user.extern_type,
151 "extern_type": user.extern_type,
152 "extern_name": user.extern_name,
152 "extern_name": user.extern_name,
153 "action": user_actions(user.user_id, user.username),
153 "action": user_actions(user.user_id, user.username),
154 })
154 })
155 data = ({
155 data = ({
156 'draw': draw,
156 'draw': draw,
157 'data': users_data,
157 'data': users_data,
158 'recordsTotal': users_data_total_count,
158 'recordsTotal': users_data_total_count,
159 'recordsFiltered': users_data_total_filtered_count,
159 'recordsFiltered': users_data_total_filtered_count,
160 'recordsTotalInactive': users_data_total_inactive_count,
160 'recordsTotalInactive': users_data_total_inactive_count,
161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 })
162 })
163
163
164 return data
164 return data
165
165
166 def _set_personal_repo_group_template_vars(self, c_obj):
166 def _set_personal_repo_group_template_vars(self, c_obj):
167 DummyUser = AttributeDict({
167 DummyUser = AttributeDict({
168 'username': '${username}',
168 'username': '${username}',
169 'user_id': '${user_id}',
169 'user_id': '${user_id}',
170 })
170 })
171 c_obj.default_create_repo_group = RepoGroupModel() \
171 c_obj.default_create_repo_group = RepoGroupModel() \
172 .get_default_create_personal_repo_group()
172 .get_default_create_personal_repo_group()
173 c_obj.personal_repo_group_name = RepoGroupModel() \
173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 .get_personal_group_name(DummyUser)
174 .get_personal_group_name(DummyUser)
175
175
176 @LoginRequired()
176 @LoginRequired()
177 @HasPermissionAllDecorator('hg.admin')
177 @HasPermissionAllDecorator('hg.admin')
178 def users_new(self):
178 def users_new(self):
179 _ = self.request.translate
179 _ = self.request.translate
180 c = self.load_default_context()
180 c = self.load_default_context()
181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 self._set_personal_repo_group_template_vars(c)
182 self._set_personal_repo_group_template_vars(c)
183 return self._get_template_context(c)
183 return self._get_template_context(c)
184
184
185 @LoginRequired()
185 @LoginRequired()
186 @HasPermissionAllDecorator('hg.admin')
186 @HasPermissionAllDecorator('hg.admin')
187 @CSRFRequired()
187 @CSRFRequired()
188 def users_create(self):
188 def users_create(self):
189 _ = self.request.translate
189 _ = self.request.translate
190 c = self.load_default_context()
190 c = self.load_default_context()
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 user_model = UserModel()
192 user_model = UserModel()
193 user_form = UserForm(self.request.translate)()
193 user_form = UserForm(self.request.translate)()
194 try:
194 try:
195 form_result = user_form.to_python(dict(self.request.POST))
195 form_result = user_form.to_python(dict(self.request.POST))
196 user = user_model.create(form_result)
196 user = user_model.create(form_result)
197 Session().flush()
197 Session().flush()
198 creation_data = user.get_api_data()
198 creation_data = user.get_api_data()
199 username = form_result['username']
199 username = form_result['username']
200
200
201 audit_logger.store_web(
201 audit_logger.store_web(
202 'user.create', action_data={'data': creation_data},
202 'user.create', action_data={'data': creation_data},
203 user=c.rhodecode_user)
203 user=c.rhodecode_user)
204
204
205 user_link = h.link_to(
205 user_link = h.link_to(
206 h.escape(username),
206 h.escape(username),
207 h.route_path('user_edit', user_id=user.user_id))
207 h.route_path('user_edit', user_id=user.user_id))
208 h.flash(h.literal(_('Created user %(user_link)s')
208 h.flash(h.literal(_('Created user %(user_link)s')
209 % {'user_link': user_link}), category='success')
209 % {'user_link': user_link}), category='success')
210 Session().commit()
210 Session().commit()
211 except formencode.Invalid as errors:
211 except formencode.Invalid as errors:
212 self._set_personal_repo_group_template_vars(c)
212 self._set_personal_repo_group_template_vars(c)
213 data = render(
213 data = render(
214 'rhodecode:templates/admin/users/user_add.mako',
214 'rhodecode:templates/admin/users/user_add.mako',
215 self._get_template_context(c), self.request)
215 self._get_template_context(c), self.request)
216 html = formencode.htmlfill.render(
216 html = formencode.htmlfill.render(
217 data,
217 data,
218 defaults=errors.value,
218 defaults=errors.value,
219 errors=errors.unpack_errors() or {},
219 errors=errors.unpack_errors() or {},
220 prefix_error=False,
220 prefix_error=False,
221 encoding="UTF-8",
221 encoding="UTF-8",
222 force_defaults=False
222 force_defaults=False
223 )
223 )
224 return Response(html)
224 return Response(html)
225 except UserCreationError as e:
225 except UserCreationError as e:
226 h.flash(safe_unicode(e), 'error')
226 h.flash(safe_unicode(e), 'error')
227 except Exception:
227 except Exception:
228 log.exception("Exception creation of user")
228 log.exception("Exception creation of user")
229 h.flash(_('Error occurred during creation of user %s')
229 h.flash(_('Error occurred during creation of user %s')
230 % self.request.POST.get('username'), category='error')
230 % self.request.POST.get('username'), category='error')
231 raise HTTPFound(h.route_path('users'))
231 raise HTTPFound(h.route_path('users'))
232
232
233
233
234 class UsersView(UserAppView):
234 class UsersView(UserAppView):
235 ALLOW_SCOPED_TOKENS = False
235 ALLOW_SCOPED_TOKENS = False
236 """
236 """
237 This view has alternative version inside EE, if modified please take a look
237 This view has alternative version inside EE, if modified please take a look
238 in there as well.
238 in there as well.
239 """
239 """
240
240
241 def get_auth_plugins(self):
241 def get_auth_plugins(self):
242 valid_plugins = []
242 valid_plugins = []
243 authn_registry = get_authn_registry(self.request.registry)
243 authn_registry = get_authn_registry(self.request.registry)
244 for plugin in authn_registry.get_plugins_for_authentication():
244 for plugin in authn_registry.get_plugins_for_authentication():
245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 valid_plugins.append(plugin)
246 valid_plugins.append(plugin)
247 elif plugin.name == 'rhodecode':
247 elif plugin.name == 'rhodecode':
248 valid_plugins.append(plugin)
248 valid_plugins.append(plugin)
249
249
250 # extend our choices if user has set a bound plugin which isn't enabled at the
250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 # moment
251 # moment
252 extern_type = self.db_user.extern_type
252 extern_type = self.db_user.extern_type
253 if extern_type not in [x.uid for x in valid_plugins]:
253 if extern_type not in [x.uid for x in valid_plugins]:
254 try:
254 try:
255 plugin = authn_registry.get_plugin_by_uid(extern_type)
255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 if plugin:
256 if plugin:
257 valid_plugins.append(plugin)
257 valid_plugins.append(plugin)
258
258
259 except Exception:
259 except Exception:
260 log.exception(
260 log.exception(
261 'Could not extend user plugins with `{}`'.format(extern_type))
261 'Could not extend user plugins with `{}`'.format(extern_type))
262 return valid_plugins
262 return valid_plugins
263
263
264 def load_default_context(self):
264 def load_default_context(self):
265 req = self.request
265 req = self.request
266
266
267 c = self._get_local_tmpl_context()
267 c = self._get_local_tmpl_context()
268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 c.allowed_languages = [
269 c.allowed_languages = [
270 ('en', 'English (en)'),
270 ('en', 'English (en)'),
271 ('de', 'German (de)'),
271 ('de', 'German (de)'),
272 ('fr', 'French (fr)'),
272 ('fr', 'French (fr)'),
273 ('it', 'Italian (it)'),
273 ('it', 'Italian (it)'),
274 ('ja', 'Japanese (ja)'),
274 ('ja', 'Japanese (ja)'),
275 ('pl', 'Polish (pl)'),
275 ('pl', 'Polish (pl)'),
276 ('pt', 'Portuguese (pt)'),
276 ('pt', 'Portuguese (pt)'),
277 ('ru', 'Russian (ru)'),
277 ('ru', 'Russian (ru)'),
278 ('zh', 'Chinese (zh)'),
278 ('zh', 'Chinese (zh)'),
279 ]
279 ]
280
280
281 c.allowed_extern_types = [
281 c.allowed_extern_types = [
282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 ]
283 ]
284 perms = req.registry.settings.get('available_permissions')
284 perms = req.registry.settings.get('available_permissions')
285 if not perms:
285 if not perms:
286 # inject info about available permissions
286 # inject info about available permissions
287 auth.set_available_permissions(req.registry.settings)
287 auth.set_available_permissions(req.registry.settings)
288
288
289 c.available_permissions = req.registry.settings['available_permissions']
289 c.available_permissions = req.registry.settings['available_permissions']
290 PermissionModel().set_global_permission_choices(
290 PermissionModel().set_global_permission_choices(
291 c, gettext_translator=req.translate)
291 c, gettext_translator=req.translate)
292
292
293 return c
293 return c
294
294
295 @LoginRequired()
295 @LoginRequired()
296 @HasPermissionAllDecorator('hg.admin')
296 @HasPermissionAllDecorator('hg.admin')
297 @CSRFRequired()
297 @CSRFRequired()
298 def user_update(self):
298 def user_update(self):
299 _ = self.request.translate
299 _ = self.request.translate
300 c = self.load_default_context()
300 c = self.load_default_context()
301
301
302 user_id = self.db_user_id
302 user_id = self.db_user_id
303 c.user = self.db_user
303 c.user = self.db_user
304
304
305 c.active = 'profile'
305 c.active = 'profile'
306 c.extern_type = c.user.extern_type
306 c.extern_type = c.user.extern_type
307 c.extern_name = c.user.extern_name
307 c.extern_name = c.user.extern_name
308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 available_languages = [x[0] for x in c.allowed_languages]
309 available_languages = [x[0] for x in c.allowed_languages]
310 _form = UserForm(self.request.translate, edit=True,
310 _form = UserForm(self.request.translate, edit=True,
311 available_languages=available_languages,
311 available_languages=available_languages,
312 old_data={'user_id': user_id,
312 old_data={'user_id': user_id,
313 'email': c.user.email})()
313 'email': c.user.email})()
314
314
315 c.edit_mode = self.request.POST.get('edit') == '1'
315 c.edit_mode = self.request.POST.get('edit') == '1'
316 form_result = {}
316 form_result = {}
317 old_values = c.user.get_api_data()
317 old_values = c.user.get_api_data()
318 try:
318 try:
319 form_result = _form.to_python(dict(self.request.POST))
319 form_result = _form.to_python(dict(self.request.POST))
320 skip_attrs = ['extern_name']
320 skip_attrs = ['extern_name']
321 # TODO: plugin should define if username can be updated
321 # TODO: plugin should define if username can be updated
322
322
323 if c.extern_type != "rhodecode" and not c.edit_mode:
323 if c.extern_type != "rhodecode" and not c.edit_mode:
324 # forbid updating username for external accounts
324 # forbid updating username for external accounts
325 skip_attrs.append('username')
325 skip_attrs.append('username')
326
326
327 UserModel().update_user(
327 UserModel().update_user(
328 user_id, skip_attrs=skip_attrs, **form_result)
328 user_id, skip_attrs=skip_attrs, **form_result)
329
329
330 audit_logger.store_web(
330 audit_logger.store_web(
331 'user.edit', action_data={'old_data': old_values},
331 'user.edit', action_data={'old_data': old_values},
332 user=c.rhodecode_user)
332 user=c.rhodecode_user)
333
333
334 Session().commit()
334 Session().commit()
335 h.flash(_('User updated successfully'), category='success')
335 h.flash(_('User updated successfully'), category='success')
336 except formencode.Invalid as errors:
336 except formencode.Invalid as errors:
337 data = render(
337 data = render(
338 'rhodecode:templates/admin/users/user_edit.mako',
338 'rhodecode:templates/admin/users/user_edit.mako',
339 self._get_template_context(c), self.request)
339 self._get_template_context(c), self.request)
340 html = formencode.htmlfill.render(
340 html = formencode.htmlfill.render(
341 data,
341 data,
342 defaults=errors.value,
342 defaults=errors.value,
343 errors=errors.unpack_errors() or {},
343 errors=errors.unpack_errors() or {},
344 prefix_error=False,
344 prefix_error=False,
345 encoding="UTF-8",
345 encoding="UTF-8",
346 force_defaults=False
346 force_defaults=False
347 )
347 )
348 return Response(html)
348 return Response(html)
349 except UserCreationError as e:
349 except UserCreationError as e:
350 h.flash(safe_unicode(e), 'error')
350 h.flash(safe_unicode(e), 'error')
351 except Exception:
351 except Exception:
352 log.exception("Exception updating user")
352 log.exception("Exception updating user")
353 h.flash(_('Error occurred during update of user %s')
353 h.flash(_('Error occurred during update of user %s')
354 % form_result.get('username'), category='error')
354 % form_result.get('username'), category='error')
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
356
356
357 @LoginRequired()
357 @LoginRequired()
358 @HasPermissionAllDecorator('hg.admin')
358 @HasPermissionAllDecorator('hg.admin')
359 @CSRFRequired()
359 @CSRFRequired()
360 def user_delete(self):
360 def user_delete(self):
361 _ = self.request.translate
361 _ = self.request.translate
362 c = self.load_default_context()
362 c = self.load_default_context()
363 c.user = self.db_user
363 c.user = self.db_user
364
364
365 _repos = c.user.repositories
365 _repos = c.user.repositories
366 _repo_groups = c.user.repository_groups
366 _repo_groups = c.user.repository_groups
367 _user_groups = c.user.user_groups
367 _user_groups = c.user.user_groups
368 _pull_requests = c.user.user_pull_requests
368 _pull_requests = c.user.user_pull_requests
369 _artifacts = c.user.artifacts
369 _artifacts = c.user.artifacts
370
370
371 handle_repos = None
371 handle_repos = None
372 handle_repo_groups = None
372 handle_repo_groups = None
373 handle_user_groups = None
373 handle_user_groups = None
374 handle_pull_requests = None
374 handle_pull_requests = None
375 handle_artifacts = None
375 handle_artifacts = None
376
376
377 # calls for flash of handle based on handle case detach or delete
377 # calls for flash of handle based on handle case detach or delete
378 def set_handle_flash_repos():
378 def set_handle_flash_repos():
379 handle = handle_repos
379 handle = handle_repos
380 if handle == 'detach':
380 if handle == 'detach':
381 h.flash(_('Detached %s repositories') % len(_repos),
381 h.flash(_('Detached %s repositories') % len(_repos),
382 category='success')
382 category='success')
383 elif handle == 'delete':
383 elif handle == 'delete':
384 h.flash(_('Deleted %s repositories') % len(_repos),
384 h.flash(_('Deleted %s repositories') % len(_repos),
385 category='success')
385 category='success')
386
386
387 def set_handle_flash_repo_groups():
387 def set_handle_flash_repo_groups():
388 handle = handle_repo_groups
388 handle = handle_repo_groups
389 if handle == 'detach':
389 if handle == 'detach':
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
391 category='success')
391 category='success')
392 elif handle == 'delete':
392 elif handle == 'delete':
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
394 category='success')
394 category='success')
395
395
396 def set_handle_flash_user_groups():
396 def set_handle_flash_user_groups():
397 handle = handle_user_groups
397 handle = handle_user_groups
398 if handle == 'detach':
398 if handle == 'detach':
399 h.flash(_('Detached %s user groups') % len(_user_groups),
399 h.flash(_('Detached %s user groups') % len(_user_groups),
400 category='success')
400 category='success')
401 elif handle == 'delete':
401 elif handle == 'delete':
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
403 category='success')
403 category='success')
404
404
405 def set_handle_flash_pull_requests():
405 def set_handle_flash_pull_requests():
406 handle = handle_pull_requests
406 handle = handle_pull_requests
407 if handle == 'detach':
407 if handle == 'detach':
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
409 category='success')
409 category='success')
410 elif handle == 'delete':
410 elif handle == 'delete':
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
412 category='success')
412 category='success')
413
413
414 def set_handle_flash_artifacts():
414 def set_handle_flash_artifacts():
415 handle = handle_artifacts
415 handle = handle_artifacts
416 if handle == 'detach':
416 if handle == 'detach':
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 category='success')
418 category='success')
419 elif handle == 'delete':
419 elif handle == 'delete':
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 category='success')
421 category='success')
422
422
423 handle_user = User.get_first_super_admin()
423 handle_user = User.get_first_super_admin()
424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
425 if handle_user_id:
425 if handle_user_id:
426 # NOTE(marcink): we get new owner for objects...
426 # NOTE(marcink): we get new owner for objects...
427 handle_user = User.get_or_404(handle_user_id)
427 handle_user = User.get_or_404(handle_user_id)
428
428
429 if _repos and self.request.POST.get('user_repos'):
429 if _repos and self.request.POST.get('user_repos'):
430 handle_repos = self.request.POST['user_repos']
430 handle_repos = self.request.POST['user_repos']
431
431
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
433 handle_repo_groups = self.request.POST['user_repo_groups']
433 handle_repo_groups = self.request.POST['user_repo_groups']
434
434
435 if _user_groups and self.request.POST.get('user_user_groups'):
435 if _user_groups and self.request.POST.get('user_user_groups'):
436 handle_user_groups = self.request.POST['user_user_groups']
436 handle_user_groups = self.request.POST['user_user_groups']
437
437
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
439 handle_pull_requests = self.request.POST['user_pull_requests']
439 handle_pull_requests = self.request.POST['user_pull_requests']
440
440
441 if _artifacts and self.request.POST.get('user_artifacts'):
441 if _artifacts and self.request.POST.get('user_artifacts'):
442 handle_artifacts = self.request.POST['user_artifacts']
442 handle_artifacts = self.request.POST['user_artifacts']
443
443
444 old_values = c.user.get_api_data()
444 old_values = c.user.get_api_data()
445
445
446 try:
446 try:
447
447
448 UserModel().delete(
448 UserModel().delete(
449 c.user,
449 c.user,
450 handle_repos=handle_repos,
450 handle_repos=handle_repos,
451 handle_repo_groups=handle_repo_groups,
451 handle_repo_groups=handle_repo_groups,
452 handle_user_groups=handle_user_groups,
452 handle_user_groups=handle_user_groups,
453 handle_pull_requests=handle_pull_requests,
453 handle_pull_requests=handle_pull_requests,
454 handle_artifacts=handle_artifacts,
454 handle_artifacts=handle_artifacts,
455 handle_new_owner=handle_user
455 handle_new_owner=handle_user
456 )
456 )
457
457
458 audit_logger.store_web(
458 audit_logger.store_web(
459 'user.delete', action_data={'old_data': old_values},
459 'user.delete', action_data={'old_data': old_values},
460 user=c.rhodecode_user)
460 user=c.rhodecode_user)
461
461
462 Session().commit()
462 Session().commit()
463 set_handle_flash_repos()
463 set_handle_flash_repos()
464 set_handle_flash_repo_groups()
464 set_handle_flash_repo_groups()
465 set_handle_flash_user_groups()
465 set_handle_flash_user_groups()
466 set_handle_flash_pull_requests()
466 set_handle_flash_pull_requests()
467 set_handle_flash_artifacts()
467 set_handle_flash_artifacts()
468 username = h.escape(old_values['username'])
468 username = h.escape(old_values['username'])
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
472 UserOwnsArtifactsException, DefaultUserException) as e:
472 UserOwnsArtifactsException, DefaultUserException) as e:
473 h.flash(e, category='warning')
473 h.flash(e, category='warning')
474 except Exception:
474 except Exception:
475 log.exception("Exception during deletion of user")
475 log.exception("Exception during deletion of user")
476 h.flash(_('An error occurred during deletion of user'),
476 h.flash(_('An error occurred during deletion of user'),
477 category='error')
477 category='error')
478 raise HTTPFound(h.route_path('users'))
478 raise HTTPFound(h.route_path('users'))
479
479
480 @LoginRequired()
480 @LoginRequired()
481 @HasPermissionAllDecorator('hg.admin')
481 @HasPermissionAllDecorator('hg.admin')
482 def user_edit(self):
482 def user_edit(self):
483 _ = self.request.translate
483 _ = self.request.translate
484 c = self.load_default_context()
484 c = self.load_default_context()
485 c.user = self.db_user
485 c.user = self.db_user
486
486
487 c.active = 'profile'
487 c.active = 'profile'
488 c.extern_type = c.user.extern_type
488 c.extern_type = c.user.extern_type
489 c.extern_name = c.user.extern_name
489 c.extern_name = c.user.extern_name
490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 c.edit_mode = self.request.GET.get('edit') == '1'
491 c.edit_mode = self.request.GET.get('edit') == '1'
492
492
493 defaults = c.user.get_dict()
493 defaults = c.user.get_dict()
494 defaults.update({'language': c.user.user_data.get('language')})
494 defaults.update({'language': c.user.user_data.get('language')})
495
495
496 data = render(
496 data = render(
497 'rhodecode:templates/admin/users/user_edit.mako',
497 'rhodecode:templates/admin/users/user_edit.mako',
498 self._get_template_context(c), self.request)
498 self._get_template_context(c), self.request)
499 html = formencode.htmlfill.render(
499 html = formencode.htmlfill.render(
500 data,
500 data,
501 defaults=defaults,
501 defaults=defaults,
502 encoding="UTF-8",
502 encoding="UTF-8",
503 force_defaults=False
503 force_defaults=False
504 )
504 )
505 return Response(html)
505 return Response(html)
506
506
507 @LoginRequired()
507 @LoginRequired()
508 @HasPermissionAllDecorator('hg.admin')
508 @HasPermissionAllDecorator('hg.admin')
509 def user_edit_advanced(self):
509 def user_edit_advanced(self):
510 _ = self.request.translate
510 _ = self.request.translate
511 c = self.load_default_context()
511 c = self.load_default_context()
512
512
513 user_id = self.db_user_id
513 user_id = self.db_user_id
514 c.user = self.db_user
514 c.user = self.db_user
515
515
516 c.detach_user = User.get_first_super_admin()
516 c.detach_user = User.get_first_super_admin()
517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
518 if detach_user_id:
518 if detach_user_id:
519 c.detach_user = User.get_or_404(detach_user_id)
519 c.detach_user = User.get_or_404(detach_user_id)
520
520
521 c.active = 'advanced'
521 c.active = 'advanced'
522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
523 c.personal_repo_group_name = RepoGroupModel()\
523 c.personal_repo_group_name = RepoGroupModel()\
524 .get_personal_group_name(c.user)
524 .get_personal_group_name(c.user)
525
525
526 c.user_to_review_rules = sorted(
526 c.user_to_review_rules = sorted(
527 (x.user for x in c.user.user_review_rules),
527 (x.user for x in c.user.user_review_rules),
528 key=lambda u: u.username.lower())
528 key=lambda u: u.username.lower())
529
529
530 defaults = c.user.get_dict()
530 defaults = c.user.get_dict()
531
531
532 # Interim workaround if the user participated on any pull requests as a
532 # Interim workaround if the user participated on any pull requests as a
533 # reviewer.
533 # reviewer.
534 has_review = len(c.user.reviewer_pull_requests)
534 has_review = len(c.user.reviewer_pull_requests)
535 c.can_delete_user = not has_review
535 c.can_delete_user = not has_review
536 c.can_delete_user_message = ''
536 c.can_delete_user_message = ''
537 inactive_link = h.link_to(
537 inactive_link = h.link_to(
538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
539 if has_review == 1:
539 if has_review == 1:
540 c.can_delete_user_message = h.literal(_(
540 c.can_delete_user_message = h.literal(_(
541 'The user participates as reviewer in {} pull request and '
541 'The user participates as reviewer in {} pull request and '
542 'cannot be deleted. \nYou can set the user to '
542 'cannot be deleted. \nYou can set the user to '
543 '"{}" instead of deleting it.').format(
543 '"{}" instead of deleting it.').format(
544 has_review, inactive_link))
544 has_review, inactive_link))
545 elif has_review:
545 elif has_review:
546 c.can_delete_user_message = h.literal(_(
546 c.can_delete_user_message = h.literal(_(
547 'The user participates as reviewer in {} pull requests and '
547 'The user participates as reviewer in {} pull requests and '
548 'cannot be deleted. \nYou can set the user to '
548 'cannot be deleted. \nYou can set the user to '
549 '"{}" instead of deleting it.').format(
549 '"{}" instead of deleting it.').format(
550 has_review, inactive_link))
550 has_review, inactive_link))
551
551
552 data = render(
552 data = render(
553 'rhodecode:templates/admin/users/user_edit.mako',
553 'rhodecode:templates/admin/users/user_edit.mako',
554 self._get_template_context(c), self.request)
554 self._get_template_context(c), self.request)
555 html = formencode.htmlfill.render(
555 html = formencode.htmlfill.render(
556 data,
556 data,
557 defaults=defaults,
557 defaults=defaults,
558 encoding="UTF-8",
558 encoding="UTF-8",
559 force_defaults=False
559 force_defaults=False
560 )
560 )
561 return Response(html)
561 return Response(html)
562
562
563 @LoginRequired()
563 @LoginRequired()
564 @HasPermissionAllDecorator('hg.admin')
564 @HasPermissionAllDecorator('hg.admin')
565 def user_edit_global_perms(self):
565 def user_edit_global_perms(self):
566 _ = self.request.translate
566 _ = self.request.translate
567 c = self.load_default_context()
567 c = self.load_default_context()
568 c.user = self.db_user
568 c.user = self.db_user
569
569
570 c.active = 'global_perms'
570 c.active = 'global_perms'
571
571
572 c.default_user = User.get_default_user()
572 c.default_user = User.get_default_user()
573 defaults = c.user.get_dict()
573 defaults = c.user.get_dict()
574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
575 defaults.update(c.default_user.get_default_perms())
575 defaults.update(c.default_user.get_default_perms())
576 defaults.update(c.user.get_default_perms())
576 defaults.update(c.user.get_default_perms())
577
577
578 data = render(
578 data = render(
579 'rhodecode:templates/admin/users/user_edit.mako',
579 'rhodecode:templates/admin/users/user_edit.mako',
580 self._get_template_context(c), self.request)
580 self._get_template_context(c), self.request)
581 html = formencode.htmlfill.render(
581 html = formencode.htmlfill.render(
582 data,
582 data,
583 defaults=defaults,
583 defaults=defaults,
584 encoding="UTF-8",
584 encoding="UTF-8",
585 force_defaults=False
585 force_defaults=False
586 )
586 )
587 return Response(html)
587 return Response(html)
588
588
589 @LoginRequired()
589 @LoginRequired()
590 @HasPermissionAllDecorator('hg.admin')
590 @HasPermissionAllDecorator('hg.admin')
591 @CSRFRequired()
591 @CSRFRequired()
592 def user_edit_global_perms_update(self):
592 def user_edit_global_perms_update(self):
593 _ = self.request.translate
593 _ = self.request.translate
594 c = self.load_default_context()
594 c = self.load_default_context()
595
595
596 user_id = self.db_user_id
596 user_id = self.db_user_id
597 c.user = self.db_user
597 c.user = self.db_user
598
598
599 c.active = 'global_perms'
599 c.active = 'global_perms'
600 try:
600 try:
601 # first stage that verifies the checkbox
601 # first stage that verifies the checkbox
602 _form = UserIndividualPermissionsForm(self.request.translate)
602 _form = UserIndividualPermissionsForm(self.request.translate)
603 form_result = _form.to_python(dict(self.request.POST))
603 form_result = _form.to_python(dict(self.request.POST))
604 inherit_perms = form_result['inherit_default_permissions']
604 inherit_perms = form_result['inherit_default_permissions']
605 c.user.inherit_default_permissions = inherit_perms
605 c.user.inherit_default_permissions = inherit_perms
606 Session().add(c.user)
606 Session().add(c.user)
607
607
608 if not inherit_perms:
608 if not inherit_perms:
609 # only update the individual ones if we un check the flag
609 # only update the individual ones if we un check the flag
610 _form = UserPermissionsForm(
610 _form = UserPermissionsForm(
611 self.request.translate,
611 self.request.translate,
612 [x[0] for x in c.repo_create_choices],
612 [x[0] for x in c.repo_create_choices],
613 [x[0] for x in c.repo_create_on_write_choices],
613 [x[0] for x in c.repo_create_on_write_choices],
614 [x[0] for x in c.repo_group_create_choices],
614 [x[0] for x in c.repo_group_create_choices],
615 [x[0] for x in c.user_group_create_choices],
615 [x[0] for x in c.user_group_create_choices],
616 [x[0] for x in c.fork_choices],
616 [x[0] for x in c.fork_choices],
617 [x[0] for x in c.inherit_default_permission_choices])()
617 [x[0] for x in c.inherit_default_permission_choices])()
618
618
619 form_result = _form.to_python(dict(self.request.POST))
619 form_result = _form.to_python(dict(self.request.POST))
620 form_result.update({'perm_user_id': c.user.user_id})
620 form_result.update({'perm_user_id': c.user.user_id})
621
621
622 PermissionModel().update_user_permissions(form_result)
622 PermissionModel().update_user_permissions(form_result)
623
623
624 # TODO(marcink): implement global permissions
624 # TODO(marcink): implement global permissions
625 # audit_log.store_web('user.edit.permissions')
625 # audit_log.store_web('user.edit.permissions')
626
626
627 Session().commit()
627 Session().commit()
628
628
629 h.flash(_('User global permissions updated successfully'),
629 h.flash(_('User global permissions updated successfully'),
630 category='success')
630 category='success')
631
631
632 except formencode.Invalid as errors:
632 except formencode.Invalid as errors:
633 data = render(
633 data = render(
634 'rhodecode:templates/admin/users/user_edit.mako',
634 'rhodecode:templates/admin/users/user_edit.mako',
635 self._get_template_context(c), self.request)
635 self._get_template_context(c), self.request)
636 html = formencode.htmlfill.render(
636 html = formencode.htmlfill.render(
637 data,
637 data,
638 defaults=errors.value,
638 defaults=errors.value,
639 errors=errors.unpack_errors() or {},
639 errors=errors.unpack_errors() or {},
640 prefix_error=False,
640 prefix_error=False,
641 encoding="UTF-8",
641 encoding="UTF-8",
642 force_defaults=False
642 force_defaults=False
643 )
643 )
644 return Response(html)
644 return Response(html)
645 except Exception:
645 except Exception:
646 log.exception("Exception during permissions saving")
646 log.exception("Exception during permissions saving")
647 h.flash(_('An error occurred during permissions saving'),
647 h.flash(_('An error occurred during permissions saving'),
648 category='error')
648 category='error')
649
649
650 affected_user_ids = [user_id]
650 affected_user_ids = [user_id]
651 PermissionModel().trigger_permission_flush(affected_user_ids)
651 PermissionModel().trigger_permission_flush(affected_user_ids)
652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
653
653
654 @LoginRequired()
654 @LoginRequired()
655 @HasPermissionAllDecorator('hg.admin')
655 @HasPermissionAllDecorator('hg.admin')
656 @CSRFRequired()
656 @CSRFRequired()
657 def user_enable_force_password_reset(self):
657 def user_enable_force_password_reset(self):
658 _ = self.request.translate
658 _ = self.request.translate
659 c = self.load_default_context()
659 c = self.load_default_context()
660
660
661 user_id = self.db_user_id
661 user_id = self.db_user_id
662 c.user = self.db_user
662 c.user = self.db_user
663
663
664 try:
664 try:
665 c.user.update_userdata(force_password_change=True)
665 c.user.update_userdata(force_password_change=True)
666
666
667 msg = _('Force password change enabled for user')
667 msg = _('Force password change enabled for user')
668 audit_logger.store_web('user.edit.password_reset.enabled',
668 audit_logger.store_web('user.edit.password_reset.enabled',
669 user=c.rhodecode_user)
669 user=c.rhodecode_user)
670
670
671 Session().commit()
671 Session().commit()
672 h.flash(msg, category='success')
672 h.flash(msg, category='success')
673 except Exception:
673 except Exception:
674 log.exception("Exception during password reset for user")
674 log.exception("Exception during password reset for user")
675 h.flash(_('An error occurred during password reset for user'),
675 h.flash(_('An error occurred during password reset for user'),
676 category='error')
676 category='error')
677
677
678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
679
679
680 @LoginRequired()
680 @LoginRequired()
681 @HasPermissionAllDecorator('hg.admin')
681 @HasPermissionAllDecorator('hg.admin')
682 @CSRFRequired()
682 @CSRFRequired()
683 def user_disable_force_password_reset(self):
683 def user_disable_force_password_reset(self):
684 _ = self.request.translate
684 _ = self.request.translate
685 c = self.load_default_context()
685 c = self.load_default_context()
686
686
687 user_id = self.db_user_id
687 user_id = self.db_user_id
688 c.user = self.db_user
688 c.user = self.db_user
689
689
690 try:
690 try:
691 c.user.update_userdata(force_password_change=False)
691 c.user.update_userdata(force_password_change=False)
692
692
693 msg = _('Force password change disabled for user')
693 msg = _('Force password change disabled for user')
694 audit_logger.store_web(
694 audit_logger.store_web(
695 'user.edit.password_reset.disabled',
695 'user.edit.password_reset.disabled',
696 user=c.rhodecode_user)
696 user=c.rhodecode_user)
697
697
698 Session().commit()
698 Session().commit()
699 h.flash(msg, category='success')
699 h.flash(msg, category='success')
700 except Exception:
700 except Exception:
701 log.exception("Exception during password reset for user")
701 log.exception("Exception during password reset for user")
702 h.flash(_('An error occurred during password reset for user'),
702 h.flash(_('An error occurred during password reset for user'),
703 category='error')
703 category='error')
704
704
705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706
706
707 @LoginRequired()
707 @LoginRequired()
708 @HasPermissionAllDecorator('hg.admin')
708 @HasPermissionAllDecorator('hg.admin')
709 @CSRFRequired()
709 @CSRFRequired()
710 def user_notice_dismiss(self):
710 def user_notice_dismiss(self):
711 _ = self.request.translate
711 _ = self.request.translate
712 c = self.load_default_context()
712 c = self.load_default_context()
713
713
714 user_id = self.db_user_id
714 user_id = self.db_user_id
715 c.user = self.db_user
715 c.user = self.db_user
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 notice = UserNotice().query()\
717 notice = UserNotice().query()\
718 .filter(UserNotice.user_id == user_id)\
718 .filter(UserNotice.user_id == user_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
720 .scalar()
720 .scalar()
721 read = False
721 read = False
722 if notice:
722 if notice:
723 notice.notice_read = True
723 notice.notice_read = True
724 Session().add(notice)
724 Session().add(notice)
725 Session().commit()
725 Session().commit()
726 read = True
726 read = True
727
727
728 return {'notice': user_notice_id, 'read': read}
728 return {'notice': user_notice_id, 'read': read}
729
729
730 @LoginRequired()
730 @LoginRequired()
731 @HasPermissionAllDecorator('hg.admin')
731 @HasPermissionAllDecorator('hg.admin')
732 @CSRFRequired()
732 @CSRFRequired()
733 def user_create_personal_repo_group(self):
733 def user_create_personal_repo_group(self):
734 """
734 """
735 Create personal repository group for this user
735 Create personal repository group for this user
736 """
736 """
737 from rhodecode.model.repo_group import RepoGroupModel
737 from rhodecode.model.repo_group import RepoGroupModel
738
738
739 _ = self.request.translate
739 _ = self.request.translate
740 c = self.load_default_context()
740 c = self.load_default_context()
741
741
742 user_id = self.db_user_id
742 user_id = self.db_user_id
743 c.user = self.db_user
743 c.user = self.db_user
744
744
745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
746 c.user.user_id)
746 c.user.user_id)
747 if personal_repo_group:
747 if personal_repo_group:
748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
749
749
750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
751 named_personal_group = RepoGroup.get_by_group_name(
751 named_personal_group = RepoGroup.get_by_group_name(
752 personal_repo_group_name)
752 personal_repo_group_name)
753 try:
753 try:
754
754
755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
756 # migrate the same named group, and mark it as personal
756 # migrate the same named group, and mark it as personal
757 named_personal_group.personal = True
757 named_personal_group.personal = True
758 Session().add(named_personal_group)
758 Session().add(named_personal_group)
759 Session().commit()
759 Session().commit()
760 msg = _('Linked repository group `%s` as personal' % (
760 msg = _('Linked repository group `%s` as personal' % (
761 personal_repo_group_name,))
761 personal_repo_group_name,))
762 h.flash(msg, category='success')
762 h.flash(msg, category='success')
763 elif not named_personal_group:
763 elif not named_personal_group:
764 RepoGroupModel().create_personal_repo_group(c.user)
764 RepoGroupModel().create_personal_repo_group(c.user)
765
765
766 msg = _('Created repository group `%s`' % (
766 msg = _('Created repository group `%s`' % (
767 personal_repo_group_name,))
767 personal_repo_group_name,))
768 h.flash(msg, category='success')
768 h.flash(msg, category='success')
769 else:
769 else:
770 msg = _('Repository group `%s` is already taken' % (
770 msg = _('Repository group `%s` is already taken' % (
771 personal_repo_group_name,))
771 personal_repo_group_name,))
772 h.flash(msg, category='warning')
772 h.flash(msg, category='warning')
773 except Exception:
773 except Exception:
774 log.exception("Exception during repository group creation")
774 log.exception("Exception during repository group creation")
775 msg = _(
775 msg = _(
776 'An error occurred during repository group creation for user')
776 'An error occurred during repository group creation for user')
777 h.flash(msg, category='error')
777 h.flash(msg, category='error')
778 Session().rollback()
778 Session().rollback()
779
779
780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
781
781
782 @LoginRequired()
782 @LoginRequired()
783 @HasPermissionAllDecorator('hg.admin')
783 @HasPermissionAllDecorator('hg.admin')
784 def auth_tokens(self):
784 def auth_tokens(self):
785 _ = self.request.translate
785 _ = self.request.translate
786 c = self.load_default_context()
786 c = self.load_default_context()
787 c.user = self.db_user
787 c.user = self.db_user
788
788
789 c.active = 'auth_tokens'
789 c.active = 'auth_tokens'
790
790
791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
792 c.role_values = [
792 c.role_values = [
793 (x, AuthTokenModel.cls._get_role_name(x))
793 (x, AuthTokenModel.cls._get_role_name(x))
794 for x in AuthTokenModel.cls.ROLES]
794 for x in AuthTokenModel.cls.ROLES]
795 c.role_options = [(c.role_values, _("Role"))]
795 c.role_options = [(c.role_values, _("Role"))]
796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
797 c.user.user_id, show_expired=True)
797 c.user.user_id, show_expired=True)
798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
799 return self._get_template_context(c)
799 return self._get_template_context(c)
800
800
801 @LoginRequired()
801 @LoginRequired()
802 @HasPermissionAllDecorator('hg.admin')
802 @HasPermissionAllDecorator('hg.admin')
803 def auth_tokens_view(self):
803 def auth_tokens_view(self):
804 _ = self.request.translate
804 _ = self.request.translate
805 c = self.load_default_context()
805 c = self.load_default_context()
806 c.user = self.db_user
806 c.user = self.db_user
807
807
808 auth_token_id = self.request.POST.get('auth_token_id')
808 auth_token_id = self.request.POST.get('auth_token_id')
809
809
810 if auth_token_id:
810 if auth_token_id:
811 token = UserApiKeys.get_or_404(auth_token_id)
811 token = UserApiKeys.get_or_404(auth_token_id)
812
812
813 return {
813 return {
814 'auth_token': token.api_key
814 'auth_token': token.api_key
815 }
815 }
816
816
817 def maybe_attach_token_scope(self, token):
817 def maybe_attach_token_scope(self, token):
818 # implemented in EE edition
818 # implemented in EE edition
819 pass
819 pass
820
820
821 @LoginRequired()
821 @LoginRequired()
822 @HasPermissionAllDecorator('hg.admin')
822 @HasPermissionAllDecorator('hg.admin')
823 @CSRFRequired()
823 @CSRFRequired()
824 def auth_tokens_add(self):
824 def auth_tokens_add(self):
825 _ = self.request.translate
825 _ = self.request.translate
826 c = self.load_default_context()
826 c = self.load_default_context()
827
827
828 user_id = self.db_user_id
828 user_id = self.db_user_id
829 c.user = self.db_user
829 c.user = self.db_user
830
830
831 user_data = c.user.get_api_data()
831 user_data = c.user.get_api_data()
832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
833 description = self.request.POST.get('description')
833 description = self.request.POST.get('description')
834 role = self.request.POST.get('role')
834 role = self.request.POST.get('role')
835
835
836 token = UserModel().add_auth_token(
836 token = UserModel().add_auth_token(
837 user=c.user.user_id,
837 user=c.user.user_id,
838 lifetime_minutes=lifetime, role=role, description=description,
838 lifetime_minutes=lifetime, role=role, description=description,
839 scope_callback=self.maybe_attach_token_scope)
839 scope_callback=self.maybe_attach_token_scope)
840 token_data = token.get_api_data()
840 token_data = token.get_api_data()
841
841
842 audit_logger.store_web(
842 audit_logger.store_web(
843 'user.edit.token.add', action_data={
843 'user.edit.token.add', action_data={
844 'data': {'token': token_data, 'user': user_data}},
844 'data': {'token': token_data, 'user': user_data}},
845 user=self._rhodecode_user, )
845 user=self._rhodecode_user, )
846 Session().commit()
846 Session().commit()
847
847
848 h.flash(_("Auth token successfully created"), category='success')
848 h.flash(_("Auth token successfully created"), category='success')
849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
850
850
851 @LoginRequired()
851 @LoginRequired()
852 @HasPermissionAllDecorator('hg.admin')
852 @HasPermissionAllDecorator('hg.admin')
853 @CSRFRequired()
853 @CSRFRequired()
854 def auth_tokens_delete(self):
854 def auth_tokens_delete(self):
855 _ = self.request.translate
855 _ = self.request.translate
856 c = self.load_default_context()
856 c = self.load_default_context()
857
857
858 user_id = self.db_user_id
858 user_id = self.db_user_id
859 c.user = self.db_user
859 c.user = self.db_user
860
860
861 user_data = c.user.get_api_data()
861 user_data = c.user.get_api_data()
862
862
863 del_auth_token = self.request.POST.get('del_auth_token')
863 del_auth_token = self.request.POST.get('del_auth_token')
864
864
865 if del_auth_token:
865 if del_auth_token:
866 token = UserApiKeys.get_or_404(del_auth_token)
866 token = UserApiKeys.get_or_404(del_auth_token)
867 token_data = token.get_api_data()
867 token_data = token.get_api_data()
868
868
869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
870 audit_logger.store_web(
870 audit_logger.store_web(
871 'user.edit.token.delete', action_data={
871 'user.edit.token.delete', action_data={
872 'data': {'token': token_data, 'user': user_data}},
872 'data': {'token': token_data, 'user': user_data}},
873 user=self._rhodecode_user,)
873 user=self._rhodecode_user,)
874 Session().commit()
874 Session().commit()
875 h.flash(_("Auth token successfully deleted"), category='success')
875 h.flash(_("Auth token successfully deleted"), category='success')
876
876
877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
878
878
879 @LoginRequired()
879 @LoginRequired()
880 @HasPermissionAllDecorator('hg.admin')
880 @HasPermissionAllDecorator('hg.admin')
881 def ssh_keys(self):
881 def ssh_keys(self):
882 _ = self.request.translate
882 _ = self.request.translate
883 c = self.load_default_context()
883 c = self.load_default_context()
884 c.user = self.db_user
884 c.user = self.db_user
885
885
886 c.active = 'ssh_keys'
886 c.active = 'ssh_keys'
887 c.default_key = self.request.GET.get('default_key')
887 c.default_key = self.request.GET.get('default_key')
888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
889 return self._get_template_context(c)
889 return self._get_template_context(c)
890
890
891 @LoginRequired()
891 @LoginRequired()
892 @HasPermissionAllDecorator('hg.admin')
892 @HasPermissionAllDecorator('hg.admin')
893 def ssh_keys_generate_keypair(self):
893 def ssh_keys_generate_keypair(self):
894 _ = self.request.translate
894 _ = self.request.translate
895 c = self.load_default_context()
895 c = self.load_default_context()
896
896
897 c.user = self.db_user
897 c.user = self.db_user
898
898
899 c.active = 'ssh_keys_generate'
899 c.active = 'ssh_keys_generate'
900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
901 private_format = self.request.GET.get('private_format') \
901 private_format = self.request.GET.get('private_format') \
902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
903 c.private, c.public = SshKeyModel().generate_keypair(
903 c.private, c.public = SshKeyModel().generate_keypair(
904 comment=comment, private_format=private_format)
904 comment=comment, private_format=private_format)
905
905
906 return self._get_template_context(c)
906 return self._get_template_context(c)
907
907
908 @LoginRequired()
908 @LoginRequired()
909 @HasPermissionAllDecorator('hg.admin')
909 @HasPermissionAllDecorator('hg.admin')
910 @CSRFRequired()
910 @CSRFRequired()
911 def ssh_keys_add(self):
911 def ssh_keys_add(self):
912 _ = self.request.translate
912 _ = self.request.translate
913 c = self.load_default_context()
913 c = self.load_default_context()
914
914
915 user_id = self.db_user_id
915 user_id = self.db_user_id
916 c.user = self.db_user
916 c.user = self.db_user
917
917
918 user_data = c.user.get_api_data()
918 user_data = c.user.get_api_data()
919 key_data = self.request.POST.get('key_data')
919 key_data = self.request.POST.get('key_data')
920 description = self.request.POST.get('description')
920 description = self.request.POST.get('description')
921
921
922 fingerprint = 'unknown'
922 fingerprint = 'unknown'
923 try:
923 try:
924 if not key_data:
924 if not key_data:
925 raise ValueError('Please add a valid public key')
925 raise ValueError('Please add a valid public key')
926
926
927 key = SshKeyModel().parse_key(key_data.strip())
927 key = SshKeyModel().parse_key(key_data.strip())
928 fingerprint = key.hash_md5()
928 fingerprint = key.hash_md5()
929
929
930 ssh_key = SshKeyModel().create(
930 ssh_key = SshKeyModel().create(
931 c.user.user_id, fingerprint, key.keydata, description)
931 c.user.user_id, fingerprint, key.keydata, description)
932 ssh_key_data = ssh_key.get_api_data()
932 ssh_key_data = ssh_key.get_api_data()
933
933
934 audit_logger.store_web(
934 audit_logger.store_web(
935 'user.edit.ssh_key.add', action_data={
935 'user.edit.ssh_key.add', action_data={
936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
937 user=self._rhodecode_user, )
937 user=self._rhodecode_user, )
938 Session().commit()
938 Session().commit()
939
939
940 # Trigger an event on change of keys.
940 # Trigger an event on change of keys.
941 trigger(SshKeyFileChangeEvent(), self.request.registry)
941 trigger(SshKeyFileChangeEvent(), self.request.registry)
942
942
943 h.flash(_("Ssh Key successfully created"), category='success')
943 h.flash(_("Ssh Key successfully created"), category='success')
944
944
945 except IntegrityError:
945 except IntegrityError:
946 log.exception("Exception during ssh key saving")
946 log.exception("Exception during ssh key saving")
947 err = 'Such key with fingerprint `{}` already exists, ' \
947 err = 'Such key with fingerprint `{}` already exists, ' \
948 'please use a different one'.format(fingerprint)
948 'please use a different one'.format(fingerprint)
949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
950 category='error')
950 category='error')
951 except Exception as e:
951 except Exception as e:
952 log.exception("Exception during ssh key saving")
952 log.exception("Exception during ssh key saving")
953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
954 category='error')
954 category='error')
955
955
956 return HTTPFound(
956 return HTTPFound(
957 h.route_path('edit_user_ssh_keys', user_id=user_id))
957 h.route_path('edit_user_ssh_keys', user_id=user_id))
958
958
959 @LoginRequired()
959 @LoginRequired()
960 @HasPermissionAllDecorator('hg.admin')
960 @HasPermissionAllDecorator('hg.admin')
961 @CSRFRequired()
961 @CSRFRequired()
962 def ssh_keys_delete(self):
962 def ssh_keys_delete(self):
963 _ = self.request.translate
963 _ = self.request.translate
964 c = self.load_default_context()
964 c = self.load_default_context()
965
965
966 user_id = self.db_user_id
966 user_id = self.db_user_id
967 c.user = self.db_user
967 c.user = self.db_user
968
968
969 user_data = c.user.get_api_data()
969 user_data = c.user.get_api_data()
970
970
971 del_ssh_key = self.request.POST.get('del_ssh_key')
971 del_ssh_key = self.request.POST.get('del_ssh_key')
972
972
973 if del_ssh_key:
973 if del_ssh_key:
974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
975 ssh_key_data = ssh_key.get_api_data()
975 ssh_key_data = ssh_key.get_api_data()
976
976
977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
978 audit_logger.store_web(
978 audit_logger.store_web(
979 'user.edit.ssh_key.delete', action_data={
979 'user.edit.ssh_key.delete', action_data={
980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
981 user=self._rhodecode_user,)
981 user=self._rhodecode_user,)
982 Session().commit()
982 Session().commit()
983 # Trigger an event on change of keys.
983 # Trigger an event on change of keys.
984 trigger(SshKeyFileChangeEvent(), self.request.registry)
984 trigger(SshKeyFileChangeEvent(), self.request.registry)
985 h.flash(_("Ssh key successfully deleted"), category='success')
985 h.flash(_("Ssh key successfully deleted"), category='success')
986
986
987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
988
988
989 @LoginRequired()
989 @LoginRequired()
990 @HasPermissionAllDecorator('hg.admin')
990 @HasPermissionAllDecorator('hg.admin')
991 def emails(self):
991 def emails(self):
992 _ = self.request.translate
992 _ = self.request.translate
993 c = self.load_default_context()
993 c = self.load_default_context()
994 c.user = self.db_user
994 c.user = self.db_user
995
995
996 c.active = 'emails'
996 c.active = 'emails'
997 c.user_email_map = UserEmailMap.query() \
997 c.user_email_map = UserEmailMap.query() \
998 .filter(UserEmailMap.user == c.user).all()
998 .filter(UserEmailMap.user == c.user).all()
999
999
1000 return self._get_template_context(c)
1000 return self._get_template_context(c)
1001
1001
1002 @LoginRequired()
1002 @LoginRequired()
1003 @HasPermissionAllDecorator('hg.admin')
1003 @HasPermissionAllDecorator('hg.admin')
1004 @CSRFRequired()
1004 @CSRFRequired()
1005 def emails_add(self):
1005 def emails_add(self):
1006 _ = self.request.translate
1006 _ = self.request.translate
1007 c = self.load_default_context()
1007 c = self.load_default_context()
1008
1008
1009 user_id = self.db_user_id
1009 user_id = self.db_user_id
1010 c.user = self.db_user
1010 c.user = self.db_user
1011
1011
1012 email = self.request.POST.get('new_email')
1012 email = self.request.POST.get('new_email')
1013 user_data = c.user.get_api_data()
1013 user_data = c.user.get_api_data()
1014 try:
1014 try:
1015
1015
1016 form = UserExtraEmailForm(self.request.translate)()
1016 form = UserExtraEmailForm(self.request.translate)()
1017 data = form.to_python({'email': email})
1017 data = form.to_python({'email': email})
1018 email = data['email']
1018 email = data['email']
1019
1019
1020 UserModel().add_extra_email(c.user.user_id, email)
1020 UserModel().add_extra_email(c.user.user_id, email)
1021 audit_logger.store_web(
1021 audit_logger.store_web(
1022 'user.edit.email.add',
1022 'user.edit.email.add',
1023 action_data={'email': email, 'user': user_data},
1023 action_data={'email': email, 'user': user_data},
1024 user=self._rhodecode_user)
1024 user=self._rhodecode_user)
1025 Session().commit()
1025 Session().commit()
1026 h.flash(_("Added new email address `%s` for user account") % email,
1026 h.flash(_("Added new email address `%s` for user account") % email,
1027 category='success')
1027 category='success')
1028 except formencode.Invalid as error:
1028 except formencode.Invalid as error:
1029 msg = error.unpack_errors()['email']
1029 msg = error.unpack_errors()['email']
1030 h.flash(h.escape(msg), category='error')
1030 h.flash(h.escape(msg), category='error')
1031 except IntegrityError:
1031 except IntegrityError:
1032 log.warning("Email %s already exists", email)
1032 log.warning("Email %s already exists", email)
1033 h.flash(_('Email `{}` is already registered for another user.').format(email),
1033 h.flash(_('Email `{}` is already registered for another user.').format(email),
1034 category='error')
1034 category='error')
1035 except Exception:
1035 except Exception:
1036 log.exception("Exception during email saving")
1036 log.exception("Exception during email saving")
1037 h.flash(_('An error occurred during email saving'),
1037 h.flash(_('An error occurred during email saving'),
1038 category='error')
1038 category='error')
1039 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1039 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1040
1040
1041 @LoginRequired()
1041 @LoginRequired()
1042 @HasPermissionAllDecorator('hg.admin')
1042 @HasPermissionAllDecorator('hg.admin')
1043 @CSRFRequired()
1043 @CSRFRequired()
1044 def emails_delete(self):
1044 def emails_delete(self):
1045 _ = self.request.translate
1045 _ = self.request.translate
1046 c = self.load_default_context()
1046 c = self.load_default_context()
1047
1047
1048 user_id = self.db_user_id
1048 user_id = self.db_user_id
1049 c.user = self.db_user
1049 c.user = self.db_user
1050
1050
1051 email_id = self.request.POST.get('del_email_id')
1051 email_id = self.request.POST.get('del_email_id')
1052 user_model = UserModel()
1052 user_model = UserModel()
1053
1053
1054 email = UserEmailMap.query().get(email_id).email
1054 email = UserEmailMap.query().get(email_id).email
1055 user_data = c.user.get_api_data()
1055 user_data = c.user.get_api_data()
1056 user_model.delete_extra_email(c.user.user_id, email_id)
1056 user_model.delete_extra_email(c.user.user_id, email_id)
1057 audit_logger.store_web(
1057 audit_logger.store_web(
1058 'user.edit.email.delete',
1058 'user.edit.email.delete',
1059 action_data={'email': email, 'user': user_data},
1059 action_data={'email': email, 'user': user_data},
1060 user=self._rhodecode_user)
1060 user=self._rhodecode_user)
1061 Session().commit()
1061 Session().commit()
1062 h.flash(_("Removed email address from user account"),
1062 h.flash(_("Removed email address from user account"),
1063 category='success')
1063 category='success')
1064 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1064 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1065
1065
1066 @LoginRequired()
1066 @LoginRequired()
1067 @HasPermissionAllDecorator('hg.admin')
1067 @HasPermissionAllDecorator('hg.admin')
1068 def ips(self):
1068 def ips(self):
1069 _ = self.request.translate
1069 _ = self.request.translate
1070 c = self.load_default_context()
1070 c = self.load_default_context()
1071 c.user = self.db_user
1071 c.user = self.db_user
1072
1072
1073 c.active = 'ips'
1073 c.active = 'ips'
1074 c.user_ip_map = UserIpMap.query() \
1074 c.user_ip_map = UserIpMap.query() \
1075 .filter(UserIpMap.user == c.user).all()
1075 .filter(UserIpMap.user == c.user).all()
1076
1076
1077 c.inherit_default_ips = c.user.inherit_default_permissions
1077 c.inherit_default_ips = c.user.inherit_default_permissions
1078 c.default_user_ip_map = UserIpMap.query() \
1078 c.default_user_ip_map = UserIpMap.query() \
1079 .filter(UserIpMap.user == User.get_default_user()).all()
1079 .filter(UserIpMap.user == User.get_default_user()).all()
1080
1080
1081 return self._get_template_context(c)
1081 return self._get_template_context(c)
1082
1082
1083 @LoginRequired()
1083 @LoginRequired()
1084 @HasPermissionAllDecorator('hg.admin')
1084 @HasPermissionAllDecorator('hg.admin')
1085 @CSRFRequired()
1085 @CSRFRequired()
1086 # NOTE(marcink): this view is allowed for default users, as we can
1086 # NOTE(marcink): this view is allowed for default users, as we can
1087 # edit their IP white list
1087 # edit their IP white list
1088 def ips_add(self):
1088 def ips_add(self):
1089 _ = self.request.translate
1089 _ = self.request.translate
1090 c = self.load_default_context()
1090 c = self.load_default_context()
1091
1091
1092 user_id = self.db_user_id
1092 user_id = self.db_user_id
1093 c.user = self.db_user
1093 c.user = self.db_user
1094
1094
1095 user_model = UserModel()
1095 user_model = UserModel()
1096 desc = self.request.POST.get('description')
1096 desc = self.request.POST.get('description')
1097 try:
1097 try:
1098 ip_list = user_model.parse_ip_range(
1098 ip_list = user_model.parse_ip_range(
1099 self.request.POST.get('new_ip'))
1099 self.request.POST.get('new_ip'))
1100 except Exception as e:
1100 except Exception as e:
1101 ip_list = []
1101 ip_list = []
1102 log.exception("Exception during ip saving")
1102 log.exception("Exception during ip saving")
1103 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1103 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1104 category='error')
1104 category='error')
1105 added = []
1105 added = []
1106 user_data = c.user.get_api_data()
1106 user_data = c.user.get_api_data()
1107 for ip in ip_list:
1107 for ip in ip_list:
1108 try:
1108 try:
1109 form = UserExtraIpForm(self.request.translate)()
1109 form = UserExtraIpForm(self.request.translate)()
1110 data = form.to_python({'ip': ip})
1110 data = form.to_python({'ip': ip})
1111 ip = data['ip']
1111 ip = data['ip']
1112
1112
1113 user_model.add_extra_ip(c.user.user_id, ip, desc)
1113 user_model.add_extra_ip(c.user.user_id, ip, desc)
1114 audit_logger.store_web(
1114 audit_logger.store_web(
1115 'user.edit.ip.add',
1115 'user.edit.ip.add',
1116 action_data={'ip': ip, 'user': user_data},
1116 action_data={'ip': ip, 'user': user_data},
1117 user=self._rhodecode_user)
1117 user=self._rhodecode_user)
1118 Session().commit()
1118 Session().commit()
1119 added.append(ip)
1119 added.append(ip)
1120 except formencode.Invalid as error:
1120 except formencode.Invalid as error:
1121 msg = error.unpack_errors()['ip']
1121 msg = error.unpack_errors()['ip']
1122 h.flash(msg, category='error')
1122 h.flash(msg, category='error')
1123 except Exception:
1123 except Exception:
1124 log.exception("Exception during ip saving")
1124 log.exception("Exception during ip saving")
1125 h.flash(_('An error occurred during ip saving'),
1125 h.flash(_('An error occurred during ip saving'),
1126 category='error')
1126 category='error')
1127 if added:
1127 if added:
1128 h.flash(
1128 h.flash(
1129 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1129 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1130 category='success')
1130 category='success')
1131 if 'default_user' in self.request.POST:
1131 if 'default_user' in self.request.POST:
1132 # case for editing global IP list we do it for 'DEFAULT' user
1132 # case for editing global IP list we do it for 'DEFAULT' user
1133 raise HTTPFound(h.route_path('admin_permissions_ips'))
1133 raise HTTPFound(h.route_path('admin_permissions_ips'))
1134 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1134 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1135
1135
1136 @LoginRequired()
1136 @LoginRequired()
1137 @HasPermissionAllDecorator('hg.admin')
1137 @HasPermissionAllDecorator('hg.admin')
1138 @CSRFRequired()
1138 @CSRFRequired()
1139 # NOTE(marcink): this view is allowed for default users, as we can
1139 # NOTE(marcink): this view is allowed for default users, as we can
1140 # edit their IP white list
1140 # edit their IP white list
1141 def ips_delete(self):
1141 def ips_delete(self):
1142 _ = self.request.translate
1142 _ = self.request.translate
1143 c = self.load_default_context()
1143 c = self.load_default_context()
1144
1144
1145 user_id = self.db_user_id
1145 user_id = self.db_user_id
1146 c.user = self.db_user
1146 c.user = self.db_user
1147
1147
1148 ip_id = self.request.POST.get('del_ip_id')
1148 ip_id = self.request.POST.get('del_ip_id')
1149 user_model = UserModel()
1149 user_model = UserModel()
1150 user_data = c.user.get_api_data()
1150 user_data = c.user.get_api_data()
1151 ip = UserIpMap.query().get(ip_id).ip_addr
1151 ip = UserIpMap.query().get(ip_id).ip_addr
1152 user_model.delete_extra_ip(c.user.user_id, ip_id)
1152 user_model.delete_extra_ip(c.user.user_id, ip_id)
1153 audit_logger.store_web(
1153 audit_logger.store_web(
1154 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1154 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1155 user=self._rhodecode_user)
1155 user=self._rhodecode_user)
1156 Session().commit()
1156 Session().commit()
1157 h.flash(_("Removed ip address from user whitelist"), category='success')
1157 h.flash(_("Removed ip address from user whitelist"), category='success')
1158
1158
1159 if 'default_user' in self.request.POST:
1159 if 'default_user' in self.request.POST:
1160 # case for editing global IP list we do it for 'DEFAULT' user
1160 # case for editing global IP list we do it for 'DEFAULT' user
1161 raise HTTPFound(h.route_path('admin_permissions_ips'))
1161 raise HTTPFound(h.route_path('admin_permissions_ips'))
1162 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1162 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1163
1163
1164 @LoginRequired()
1164 @LoginRequired()
1165 @HasPermissionAllDecorator('hg.admin')
1165 @HasPermissionAllDecorator('hg.admin')
1166 def groups_management(self):
1166 def groups_management(self):
1167 c = self.load_default_context()
1167 c = self.load_default_context()
1168 c.user = self.db_user
1168 c.user = self.db_user
1169 c.data = c.user.group_member
1169 c.data = c.user.group_member
1170
1170
1171 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1171 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1172 for group in c.user.group_member]
1172 for group in c.user.group_member]
1173 c.groups = ext_json.str_json(groups)
1173 c.groups = ext_json.str_json(groups)
1174 c.active = 'groups'
1174 c.active = 'groups'
1175
1175
1176 return self._get_template_context(c)
1176 return self._get_template_context(c)
1177
1177
1178 @LoginRequired()
1178 @LoginRequired()
1179 @HasPermissionAllDecorator('hg.admin')
1179 @HasPermissionAllDecorator('hg.admin')
1180 @CSRFRequired()
1180 @CSRFRequired()
1181 def groups_management_updates(self):
1181 def groups_management_updates(self):
1182 _ = self.request.translate
1182 _ = self.request.translate
1183 c = self.load_default_context()
1183 c = self.load_default_context()
1184
1184
1185 user_id = self.db_user_id
1185 user_id = self.db_user_id
1186 c.user = self.db_user
1186 c.user = self.db_user
1187
1187
1188 user_groups = set(self.request.POST.getall('users_group_id'))
1188 user_groups = set(self.request.POST.getall('users_group_id'))
1189 user_groups_objects = []
1189 user_groups_objects = []
1190
1190
1191 for ugid in user_groups:
1191 for ugid in user_groups:
1192 user_groups_objects.append(
1192 user_groups_objects.append(
1193 UserGroupModel().get_group(safe_int(ugid)))
1193 UserGroupModel().get_group(safe_int(ugid)))
1194 user_group_model = UserGroupModel()
1194 user_group_model = UserGroupModel()
1195 added_to_groups, removed_from_groups = \
1195 added_to_groups, removed_from_groups = \
1196 user_group_model.change_groups(c.user, user_groups_objects)
1196 user_group_model.change_groups(c.user, user_groups_objects)
1197
1197
1198 user_data = c.user.get_api_data()
1198 user_data = c.user.get_api_data()
1199 for user_group_id in added_to_groups:
1199 for user_group_id in added_to_groups:
1200 user_group = UserGroup.get(user_group_id)
1200 user_group = UserGroup.get(user_group_id)
1201 old_values = user_group.get_api_data()
1201 old_values = user_group.get_api_data()
1202 audit_logger.store_web(
1202 audit_logger.store_web(
1203 'user_group.edit.member.add',
1203 'user_group.edit.member.add',
1204 action_data={'user': user_data, 'old_data': old_values},
1204 action_data={'user': user_data, 'old_data': old_values},
1205 user=self._rhodecode_user)
1205 user=self._rhodecode_user)
1206
1206
1207 for user_group_id in removed_from_groups:
1207 for user_group_id in removed_from_groups:
1208 user_group = UserGroup.get(user_group_id)
1208 user_group = UserGroup.get(user_group_id)
1209 old_values = user_group.get_api_data()
1209 old_values = user_group.get_api_data()
1210 audit_logger.store_web(
1210 audit_logger.store_web(
1211 'user_group.edit.member.delete',
1211 'user_group.edit.member.delete',
1212 action_data={'user': user_data, 'old_data': old_values},
1212 action_data={'user': user_data, 'old_data': old_values},
1213 user=self._rhodecode_user)
1213 user=self._rhodecode_user)
1214
1214
1215 Session().commit()
1215 Session().commit()
1216 c.active = 'user_groups_management'
1216 c.active = 'user_groups_management'
1217 h.flash(_("Groups successfully changed"), category='success')
1217 h.flash(_("Groups successfully changed"), category='success')
1218
1218
1219 return HTTPFound(h.route_path(
1219 return HTTPFound(h.route_path(
1220 'edit_user_groups_management', user_id=user_id))
1220 'edit_user_groups_management', user_id=user_id))
1221
1221
1222 @LoginRequired()
1222 @LoginRequired()
1223 @HasPermissionAllDecorator('hg.admin')
1223 @HasPermissionAllDecorator('hg.admin')
1224 def user_audit_logs(self):
1224 def user_audit_logs(self):
1225 _ = self.request.translate
1225 _ = self.request.translate
1226 c = self.load_default_context()
1226 c = self.load_default_context()
1227 c.user = self.db_user
1227 c.user = self.db_user
1228
1228
1229 c.active = 'audit'
1229 c.active = 'audit'
1230
1230
1231 p = safe_int(self.request.GET.get('page', 1), 1)
1231 p = safe_int(self.request.GET.get('page', 1), 1)
1232
1232
1233 filter_term = self.request.GET.get('filter')
1233 filter_term = self.request.GET.get('filter')
1234 user_log = UserModel().get_user_log(c.user, filter_term)
1234 user_log = UserModel().get_user_log(c.user, filter_term)
1235
1235
1236 def url_generator(page_num):
1236 def url_generator(page_num):
1237 query_params = {
1237 query_params = {
1238 'page': page_num
1238 'page': page_num
1239 }
1239 }
1240 if filter_term:
1240 if filter_term:
1241 query_params['filter'] = filter_term
1241 query_params['filter'] = filter_term
1242 return self.request.current_route_path(_query=query_params)
1242 return self.request.current_route_path(_query=query_params)
1243
1243
1244 c.audit_logs = SqlPage(
1244 c.audit_logs = SqlPage(
1245 user_log, page=p, items_per_page=10, url_maker=url_generator)
1245 user_log, page=p, items_per_page=10, url_maker=url_generator)
1246 c.filter_term = filter_term
1246 c.filter_term = filter_term
1247 return self._get_template_context(c)
1247 return self._get_template_context(c)
1248
1248
1249 @LoginRequired()
1249 @LoginRequired()
1250 @HasPermissionAllDecorator('hg.admin')
1250 @HasPermissionAllDecorator('hg.admin')
1251 def user_audit_logs_download(self):
1251 def user_audit_logs_download(self):
1252 _ = self.request.translate
1252 _ = self.request.translate
1253 c = self.load_default_context()
1253 c = self.load_default_context()
1254 c.user = self.db_user
1254 c.user = self.db_user
1255
1255
1256 user_log = UserModel().get_user_log(c.user, filter_term=None)
1256 user_log = UserModel().get_user_log(c.user, filter_term=None)
1257
1257
1258 audit_log_data = {}
1258 audit_log_data = {}
1259 for entry in user_log:
1259 for entry in user_log:
1260 audit_log_data[entry.user_log_id] = entry.get_dict()
1260 audit_log_data[entry.user_log_id] = entry.get_dict()
1261
1261
1262 response = Response(ext_json.formatted_str_json(audit_log_data))
1262 response = Response(ext_json.formatted_str_json(audit_log_data))
1263 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1263 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1264 response.content_type = 'application/json'
1264 response.content_type = 'application/json'
1265
1265
1266 return response
1266 return response
1267
1267
1268 @LoginRequired()
1268 @LoginRequired()
1269 @HasPermissionAllDecorator('hg.admin')
1269 @HasPermissionAllDecorator('hg.admin')
1270 def user_perms_summary(self):
1270 def user_perms_summary(self):
1271 _ = self.request.translate
1271 _ = self.request.translate
1272 c = self.load_default_context()
1272 c = self.load_default_context()
1273 c.user = self.db_user
1273 c.user = self.db_user
1274
1274
1275 c.active = 'perms_summary'
1275 c.active = 'perms_summary'
1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1277
1277
1278 return self._get_template_context(c)
1278 return self._get_template_context(c)
1279
1279
1280 @LoginRequired()
1280 @LoginRequired()
1281 @HasPermissionAllDecorator('hg.admin')
1281 @HasPermissionAllDecorator('hg.admin')
1282 def user_perms_summary_json(self):
1282 def user_perms_summary_json(self):
1283 self.load_default_context()
1283 self.load_default_context()
1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1285
1285
1286 return perm_user.permissions
1286 return perm_user.permissions
1287
1287
1288 @LoginRequired()
1288 @LoginRequired()
1289 @HasPermissionAllDecorator('hg.admin')
1289 @HasPermissionAllDecorator('hg.admin')
1290 def user_caches(self):
1290 def user_caches(self):
1291 _ = self.request.translate
1291 _ = self.request.translate
1292 c = self.load_default_context()
1292 c = self.load_default_context()
1293 c.user = self.db_user
1293 c.user = self.db_user
1294
1294
1295 c.active = 'caches'
1295 c.active = 'caches'
1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1297
1297
1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1300 c.backend = c.region.backend
1300 c.backend = c.region.backend
1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1302
1302
1303 return self._get_template_context(c)
1303 return self._get_template_context(c)
1304
1304
1305 @LoginRequired()
1305 @LoginRequired()
1306 @HasPermissionAllDecorator('hg.admin')
1306 @HasPermissionAllDecorator('hg.admin')
1307 @CSRFRequired()
1307 @CSRFRequired()
1308 def user_caches_update(self):
1308 def user_caches_update(self):
1309 _ = self.request.translate
1309 _ = self.request.translate
1310 c = self.load_default_context()
1310 c = self.load_default_context()
1311 c.user = self.db_user
1311 c.user = self.db_user
1312
1312
1313 c.active = 'caches'
1313 c.active = 'caches'
1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1315
1315
1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1318
1318
1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1320
1320
1321 return HTTPFound(h.route_path(
1321 return HTTPFound(h.route_path(
1322 'edit_user_caches', user_id=c.user.user_id))
1322 'edit_user_caches', user_id=c.user.user_id))
@@ -1,107 +1,106 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import os
20 import os
22
21
23 from pyramid.events import ApplicationCreated
22 from pyramid.events import ApplicationCreated
24 from pyramid.settings import asbool
23 from pyramid.settings import asbool
25
24
26 from rhodecode.apps._base import ADMIN_PREFIX
25 from rhodecode.apps._base import ADMIN_PREFIX
27 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
28 from rhodecode.lib.str_utils import safe_str
27 from rhodecode.lib.str_utils import safe_str
29
28
30
29
31 def url_gen(request):
30 def url_gen(request):
32 registry = request.registry
31 registry = request.registry
33 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
34 ws_url = registry.settings.get('channelstream.ws_url', '')
33 ws_url = registry.settings.get('channelstream.ws_url', '')
35 proxy_url = request.route_url('channelstream_proxy')
34 proxy_url = request.route_url('channelstream_proxy')
36 urls = {
35 urls = {
37 'connect': request.route_path('channelstream_connect'),
36 'connect': request.route_path('channelstream_connect'),
38 'subscribe': request.route_path('channelstream_subscribe'),
37 'subscribe': request.route_path('channelstream_subscribe'),
39 'longpoll': longpoll_url or proxy_url,
38 'longpoll': longpoll_url or proxy_url,
40 'ws': ws_url or proxy_url.replace('http', 'ws')
39 'ws': ws_url or proxy_url.replace('http', 'ws')
41 }
40 }
42 return safe_str(json.dumps(urls))
41 return safe_str(json.dumps(urls))
43
42
44
43
45 PLUGIN_DEFINITION = {
44 PLUGIN_DEFINITION = {
46 'name': 'channelstream',
45 'name': 'channelstream',
47 'config': {
46 'config': {
48 'javascript': [],
47 'javascript': [],
49 'css': [],
48 'css': [],
50 'template_hooks': {
49 'template_hooks': {
51 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
52 },
51 },
53 'url_gen': url_gen,
52 'url_gen': url_gen,
54 'static': None,
53 'static': None,
55 'enabled': False,
54 'enabled': False,
56 'server': '',
55 'server': '',
57 'secret': ''
56 'secret': ''
58 }
57 }
59 }
58 }
60
59
61
60
62 def maybe_create_history_store(event):
61 def maybe_create_history_store(event):
63 # create plugin history location
62 # create plugin history location
64 settings = event.app.registry.settings
63 settings = event.app.registry.settings
65 history_dir = settings.get('channelstream.history.location', '')
64 history_dir = settings.get('channelstream.history.location', '')
66 if history_dir and not os.path.exists(history_dir):
65 if history_dir and not os.path.exists(history_dir):
67 os.makedirs(history_dir, 0o750)
66 os.makedirs(history_dir, 0o750)
68
67
69
68
70 def includeme(config):
69 def includeme(config):
71 from rhodecode.apps.channelstream.views import ChannelstreamView
70 from rhodecode.apps.channelstream.views import ChannelstreamView
72
71
73 settings = config.registry.settings
72 settings = config.registry.settings
74 PLUGIN_DEFINITION['config']['enabled'] = asbool(
73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
75 settings.get('channelstream.enabled'))
74 settings.get('channelstream.enabled'))
76 PLUGIN_DEFINITION['config']['server'] = settings.get(
75 PLUGIN_DEFINITION['config']['server'] = settings.get(
77 'channelstream.server', '')
76 'channelstream.server', '')
78 PLUGIN_DEFINITION['config']['secret'] = settings.get(
77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
79 'channelstream.secret', '')
78 'channelstream.secret', '')
80 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
81 'channelstream.history.location', '')
80 'channelstream.history.location', '')
82 config.register_rhodecode_plugin(
81 config.register_rhodecode_plugin(
83 PLUGIN_DEFINITION['name'],
82 PLUGIN_DEFINITION['name'],
84 PLUGIN_DEFINITION['config']
83 PLUGIN_DEFINITION['config']
85 )
84 )
86 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
87
86
88 config.add_route(
87 config.add_route(
89 name='channelstream_connect',
88 name='channelstream_connect',
90 pattern=ADMIN_PREFIX + '/channelstream/connect')
89 pattern=ADMIN_PREFIX + '/channelstream/connect')
91 config.add_view(
90 config.add_view(
92 ChannelstreamView,
91 ChannelstreamView,
93 attr='channelstream_connect',
92 attr='channelstream_connect',
94 route_name='channelstream_connect', renderer='json_ext')
93 route_name='channelstream_connect', renderer='json_ext')
95
94
96 config.add_route(
95 config.add_route(
97 name='channelstream_subscribe',
96 name='channelstream_subscribe',
98 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
99 config.add_view(
98 config.add_view(
100 ChannelstreamView,
99 ChannelstreamView,
101 attr='channelstream_subscribe',
100 attr='channelstream_subscribe',
102 route_name='channelstream_subscribe', renderer='json_ext')
101 route_name='channelstream_subscribe', renderer='json_ext')
103
102
104 config.add_route(
103 config.add_route(
105 name='channelstream_proxy',
104 name='channelstream_proxy',
106 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
107
106
@@ -1,185 +1,184 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import logging
20 import logging
22 import uuid
21 import uuid
23
22
24
23
25 from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
26
25
27 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib.channelstream import (
27 from rhodecode.lib.channelstream import (
29 channelstream_request, get_channelstream_server_url,
28 channelstream_request, get_channelstream_server_url,
30 ChannelstreamConnectionException,
29 ChannelstreamConnectionException,
31 ChannelstreamPermissionException,
30 ChannelstreamPermissionException,
32 check_channel_permissions,
31 check_channel_permissions,
33 get_connection_validators,
32 get_connection_validators,
34 get_user_data,
33 get_user_data,
35 parse_channels_info,
34 parse_channels_info,
36 update_history_from_logs,
35 update_history_from_logs,
37 USER_STATE_PUBLIC_KEYS)
36 USER_STATE_PUBLIC_KEYS)
38
37
39 from rhodecode.lib.auth import NotAnonymous
38 from rhodecode.lib.auth import NotAnonymous
40
39
41 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
42
41
43
42
44 class ChannelstreamView(BaseAppView):
43 class ChannelstreamView(BaseAppView):
45
44
46 def load_default_context(self):
45 def load_default_context(self):
47 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
48 self.channelstream_config = \
47 self.channelstream_config = \
49 self.request.registry.rhodecode_plugins['channelstream']
48 self.request.registry.rhodecode_plugins['channelstream']
50 if not self.channelstream_config.get('enabled'):
49 if not self.channelstream_config.get('enabled'):
51 log.warning('Channelstream plugin is disabled')
50 log.warning('Channelstream plugin is disabled')
52 raise HTTPBadRequest()
51 raise HTTPBadRequest()
53
52
54 return c
53 return c
55
54
56 @NotAnonymous()
55 @NotAnonymous()
57 def channelstream_connect(self):
56 def channelstream_connect(self):
58 """ handle authorization of users trying to connect """
57 """ handle authorization of users trying to connect """
59
58
60 self.load_default_context()
59 self.load_default_context()
61 try:
60 try:
62 json_body = self.request.json_body
61 json_body = self.request.json_body
63 except Exception:
62 except Exception:
64 log.exception('Failed to decode json from request')
63 log.exception('Failed to decode json from request')
65 raise HTTPBadRequest()
64 raise HTTPBadRequest()
66
65
67 try:
66 try:
68 channels = check_channel_permissions(
67 channels = check_channel_permissions(
69 json_body.get('channels'),
68 json_body.get('channels'),
70 get_connection_validators(self.request.registry))
69 get_connection_validators(self.request.registry))
71 except ChannelstreamPermissionException:
70 except ChannelstreamPermissionException:
72 log.error('Incorrect permissions for requested channels')
71 log.error('Incorrect permissions for requested channels')
73 raise HTTPForbidden()
72 raise HTTPForbidden()
74
73
75 user = self._rhodecode_user
74 user = self._rhodecode_user
76 if user.user_id:
75 if user.user_id:
77 user_data = get_user_data(user.user_id)
76 user_data = get_user_data(user.user_id)
78 else:
77 else:
79 user_data = {
78 user_data = {
80 'id': None,
79 'id': None,
81 'username': None,
80 'username': None,
82 'first_name': None,
81 'first_name': None,
83 'last_name': None,
82 'last_name': None,
84 'icon_link': None,
83 'icon_link': None,
85 'display_name': None,
84 'display_name': None,
86 'display_link': None,
85 'display_link': None,
87 }
86 }
88
87
89 #user_data['permissions'] = self._rhodecode_user.permissions_safe
88 #user_data['permissions'] = self._rhodecode_user.permissions_safe
90
89
91 payload = {
90 payload = {
92 'username': user.username,
91 'username': user.username,
93 'user_state': user_data,
92 'user_state': user_data,
94 'conn_id': str(uuid.uuid4()),
93 'conn_id': str(uuid.uuid4()),
95 'channels': channels,
94 'channels': channels,
96 'channel_configs': {},
95 'channel_configs': {},
97 'state_public_keys': USER_STATE_PUBLIC_KEYS,
96 'state_public_keys': USER_STATE_PUBLIC_KEYS,
98 'info': {
97 'info': {
99 'exclude_channels': ['broadcast']
98 'exclude_channels': ['broadcast']
100 }
99 }
101 }
100 }
102 filtered_channels = [channel for channel in channels
101 filtered_channels = [channel for channel in channels
103 if channel != 'broadcast']
102 if channel != 'broadcast']
104 for channel in filtered_channels:
103 for channel in filtered_channels:
105 payload['channel_configs'][channel] = {
104 payload['channel_configs'][channel] = {
106 'notify_presence': True,
105 'notify_presence': True,
107 'history_size': 100,
106 'history_size': 100,
108 'store_history': True,
107 'store_history': True,
109 'broadcast_presence_with_user_lists': True
108 'broadcast_presence_with_user_lists': True
110 }
109 }
111 # connect user to server
110 # connect user to server
112 channelstream_url = get_channelstream_server_url(
111 channelstream_url = get_channelstream_server_url(
113 self.channelstream_config, '/connect')
112 self.channelstream_config, '/connect')
114 try:
113 try:
115 connect_result = channelstream_request(
114 connect_result = channelstream_request(
116 self.channelstream_config, payload, '/connect')
115 self.channelstream_config, payload, '/connect')
117 except ChannelstreamConnectionException:
116 except ChannelstreamConnectionException:
118 log.exception(
117 log.exception(
119 'Channelstream service at {} is down'.format(channelstream_url))
118 'Channelstream service at {} is down'.format(channelstream_url))
120 return HTTPBadGateway()
119 return HTTPBadGateway()
121
120
122 channel_info = connect_result.get('channels_info')
121 channel_info = connect_result.get('channels_info')
123 if not channel_info:
122 if not channel_info:
124 raise HTTPBadRequest()
123 raise HTTPBadRequest()
125
124
126 connect_result['channels'] = channels
125 connect_result['channels'] = channels
127 connect_result['channels_info'] = parse_channels_info(
126 connect_result['channels_info'] = parse_channels_info(
128 channel_info, include_channel_info=filtered_channels)
127 channel_info, include_channel_info=filtered_channels)
129 update_history_from_logs(self.channelstream_config,
128 update_history_from_logs(self.channelstream_config,
130 filtered_channels, connect_result)
129 filtered_channels, connect_result)
131 return connect_result
130 return connect_result
132
131
133 @NotAnonymous()
132 @NotAnonymous()
134 def channelstream_subscribe(self):
133 def channelstream_subscribe(self):
135 """ can be used to subscribe specific connection to other channels """
134 """ can be used to subscribe specific connection to other channels """
136 self.load_default_context()
135 self.load_default_context()
137 try:
136 try:
138 json_body = self.request.json_body
137 json_body = self.request.json_body
139 except Exception:
138 except Exception:
140 log.exception('Failed to decode json from request')
139 log.exception('Failed to decode json from request')
141 raise HTTPBadRequest()
140 raise HTTPBadRequest()
142 try:
141 try:
143 channels = check_channel_permissions(
142 channels = check_channel_permissions(
144 json_body.get('channels'),
143 json_body.get('channels'),
145 get_connection_validators(self.request.registry))
144 get_connection_validators(self.request.registry))
146 except ChannelstreamPermissionException:
145 except ChannelstreamPermissionException:
147 log.error('Incorrect permissions for requested channels')
146 log.error('Incorrect permissions for requested channels')
148 raise HTTPForbidden()
147 raise HTTPForbidden()
149 payload = {'conn_id': json_body.get('conn_id', ''),
148 payload = {'conn_id': json_body.get('conn_id', ''),
150 'channels': channels,
149 'channels': channels,
151 'channel_configs': {},
150 'channel_configs': {},
152 'info': {
151 'info': {
153 'exclude_channels': ['broadcast']}
152 'exclude_channels': ['broadcast']}
154 }
153 }
155 filtered_channels = [chan for chan in channels if chan != 'broadcast']
154 filtered_channels = [chan for chan in channels if chan != 'broadcast']
156 for channel in filtered_channels:
155 for channel in filtered_channels:
157 payload['channel_configs'][channel] = {
156 payload['channel_configs'][channel] = {
158 'notify_presence': True,
157 'notify_presence': True,
159 'history_size': 100,
158 'history_size': 100,
160 'store_history': True,
159 'store_history': True,
161 'broadcast_presence_with_user_lists': True
160 'broadcast_presence_with_user_lists': True
162 }
161 }
163
162
164 channelstream_url = get_channelstream_server_url(
163 channelstream_url = get_channelstream_server_url(
165 self.channelstream_config, '/subscribe')
164 self.channelstream_config, '/subscribe')
166 try:
165 try:
167 connect_result = channelstream_request(
166 connect_result = channelstream_request(
168 self.channelstream_config, payload, '/subscribe')
167 self.channelstream_config, payload, '/subscribe')
169 except ChannelstreamConnectionException:
168 except ChannelstreamConnectionException:
170 log.exception(
169 log.exception(
171 'Channelstream service at {} is down'.format(channelstream_url))
170 'Channelstream service at {} is down'.format(channelstream_url))
172 return HTTPBadGateway()
171 return HTTPBadGateway()
173
172
174 channel_info = connect_result.get('channels_info')
173 channel_info = connect_result.get('channels_info')
175 if not channel_info:
174 if not channel_info:
176 raise HTTPBadRequest()
175 raise HTTPBadRequest()
177
176
178 # include_channel_info will limit history only to new channel
177 # include_channel_info will limit history only to new channel
179 # to not overwrite histories on other channels in client
178 # to not overwrite histories on other channels in client
180 connect_result['channels_info'] = parse_channels_info(
179 connect_result['channels_info'] = parse_channels_info(
181 channel_info,
180 channel_info,
182 include_channel_info=filtered_channels)
181 include_channel_info=filtered_channels)
183 update_history_from_logs(
182 update_history_from_logs(
184 self.channelstream_config, filtered_channels, connect_result)
183 self.channelstream_config, filtered_channels, connect_result)
185 return connect_result
184 return connect_result
@@ -1,81 +1,81 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import ADMIN_PREFIX
20 from rhodecode.apps._base import ADMIN_PREFIX
21 from rhodecode.lib.utils2 import str2bool
21 from rhodecode.lib.utils2 import str2bool
22
22
23
23
24 class DebugStylePredicate(object):
24 class DebugStylePredicate(object):
25 def __init__(self, val, config):
25 def __init__(self, val, config):
26 self.val = val
26 self.val = val
27
27
28 def text(self):
28 def text(self):
29 return f'debug style route = {self.val}'
29 return f'debug style route = {self.val}'
30
30
31 phash = text
31 phash = text
32
32
33 def __call__(self, info, request):
33 def __call__(self, info, request):
34 return str2bool(request.registry.settings.get('debug_style'))
34 return str2bool(request.registry.settings.get('debug_style'))
35
35
36
36
37 def includeme(config):
37 def includeme(config):
38 from rhodecode.apps.debug_style.views import DebugStyleView
38 from rhodecode.apps.debug_style.views import DebugStyleView
39
39
40 config.add_route_predicate(
40 config.add_route_predicate(
41 'debug_style', DebugStylePredicate)
41 'debug_style', DebugStylePredicate)
42
42
43 config.add_route(
43 config.add_route(
44 name='debug_style_home',
44 name='debug_style_home',
45 pattern=ADMIN_PREFIX + '/debug_style',
45 pattern=ADMIN_PREFIX + '/debug_style',
46 debug_style=True)
46 debug_style=True)
47 config.add_view(
47 config.add_view(
48 DebugStyleView,
48 DebugStyleView,
49 attr='index',
49 attr='index',
50 route_name='debug_style_home', request_method='GET',
50 route_name='debug_style_home', request_method='GET',
51 renderer=None)
51 renderer=None)
52
52
53 config.add_route(
53 config.add_route(
54 name='debug_style_email',
54 name='debug_style_email',
55 pattern=ADMIN_PREFIX + '/debug_style/email/{email_id}',
55 pattern=ADMIN_PREFIX + '/debug_style/email/{email_id}',
56 debug_style=True)
56 debug_style=True)
57 config.add_view(
57 config.add_view(
58 DebugStyleView,
58 DebugStyleView,
59 attr='render_email',
59 attr='render_email',
60 route_name='debug_style_email', request_method='GET',
60 route_name='debug_style_email', request_method='GET',
61 renderer=None)
61 renderer=None)
62
62
63 config.add_route(
63 config.add_route(
64 name='debug_style_email_plain_rendered',
64 name='debug_style_email_plain_rendered',
65 pattern=ADMIN_PREFIX + '/debug_style/email-rendered/{email_id}',
65 pattern=ADMIN_PREFIX + '/debug_style/email-rendered/{email_id}',
66 debug_style=True)
66 debug_style=True)
67 config.add_view(
67 config.add_view(
68 DebugStyleView,
68 DebugStyleView,
69 attr='render_email',
69 attr='render_email',
70 route_name='debug_style_email_plain_rendered', request_method='GET',
70 route_name='debug_style_email_plain_rendered', request_method='GET',
71 renderer=None)
71 renderer=None)
72
72
73 config.add_route(
73 config.add_route(
74 name='debug_style_template',
74 name='debug_style_template',
75 pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',
75 pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',
76 debug_style=True)
76 debug_style=True)
77 config.add_view(
77 config.add_view(
78 DebugStyleView,
78 DebugStyleView,
79 attr='template',
79 attr='template',
80 route_name='debug_style_template', request_method='GET',
80 route_name='debug_style_template', request_method='GET',
81 renderer=None)
81 renderer=None)
@@ -1,476 +1,476 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import logging
22 import logging
23 import datetime
23 import datetime
24
24
25 from pyramid.renderers import render_to_response
25 from pyramid.renderers import render_to_response
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib.celerylib import run_task, tasks
27 from rhodecode.lib.celerylib import run_task, tasks
28 from rhodecode.lib.utils2 import AttributeDict
28 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.model.notification import EmailNotificationModel
30 from rhodecode.model.notification import EmailNotificationModel
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class DebugStyleView(BaseAppView):
35 class DebugStyleView(BaseAppView):
36
36
37 def load_default_context(self):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
38 c = self._get_local_tmpl_context()
39 return c
39 return c
40
40
41 def index(self):
41 def index(self):
42 c = self.load_default_context()
42 c = self.load_default_context()
43 c.active = 'index'
43 c.active = 'index'
44
44
45 return render_to_response(
45 return render_to_response(
46 'debug_style/index.html', self._get_template_context(c),
46 'debug_style/index.html', self._get_template_context(c),
47 request=self.request)
47 request=self.request)
48
48
49 def render_email(self):
49 def render_email(self):
50 c = self.load_default_context()
50 c = self.load_default_context()
51 email_id = self.request.matchdict['email_id']
51 email_id = self.request.matchdict['email_id']
52 c.active = 'emails'
52 c.active = 'emails'
53
53
54 pr = AttributeDict(
54 pr = AttributeDict(
55 pull_request_id=123,
55 pull_request_id=123,
56 title='digital_ocean: fix redis, elastic search start on boot, '
56 title='digital_ocean: fix redis, elastic search start on boot, '
57 'fix fd limits on supervisor, set postgres 11 version',
57 'fix fd limits on supervisor, set postgres 11 version',
58 description='''
58 description='''
59 Check if we should use full-topic or mini-topic.
59 Check if we should use full-topic or mini-topic.
60
60
61 - full topic produces some problems with merge states etc
61 - full topic produces some problems with merge states etc
62 - server-mini-topic needs probably tweeks.
62 - server-mini-topic needs probably tweeks.
63 ''',
63 ''',
64 repo_name='foobar',
64 repo_name='foobar',
65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
66 target_ref_parts=AttributeDict(type='branch', name='master'),
66 target_ref_parts=AttributeDict(type='branch', name='master'),
67 )
67 )
68
68
69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
72 # file/commit changes for PR update
72 # file/commit changes for PR update
73 commit_changes = AttributeDict({
73 commit_changes = AttributeDict({
74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
75 'removed': ['eeeeeeeeeee'],
75 'removed': ['eeeeeeeeeee'],
76 })
76 })
77
77
78 file_changes = AttributeDict({
78 file_changes = AttributeDict({
79 'added': ['a/file1.md', 'file2.py'],
79 'added': ['a/file1.md', 'file2.py'],
80 'modified': ['b/modified_file.rst'],
80 'modified': ['b/modified_file.rst'],
81 'removed': ['.idea'],
81 'removed': ['.idea'],
82 })
82 })
83
83
84 exc_traceback = {
84 exc_traceback = {
85 'exc_utc_date': '2020-03-26T12:54:50.683281',
85 'exc_utc_date': '2020-03-26T12:54:50.683281',
86 'exc_id': 139638856342656,
86 'exc_id': 139638856342656,
87 'exc_timestamp': '1585227290.683288',
87 'exc_timestamp': '1585227290.683288',
88 'version': 'v1',
88 'version': 'v1',
89 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
89 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
90 'exc_type': 'AttributeError'
90 'exc_type': 'AttributeError'
91 }
91 }
92
92
93 email_kwargs = {
93 email_kwargs = {
94 'test': {},
94 'test': {},
95
95
96 'message': {
96 'message': {
97 'body': 'message body !'
97 'body': 'message body !'
98 },
98 },
99
99
100 'email_test': {
100 'email_test': {
101 'user': user,
101 'user': user,
102 'date': datetime.datetime.now(),
102 'date': datetime.datetime.now(),
103 },
103 },
104
104
105 'update_available': {
105 'update_available': {
106 'current_ver': '4.23.0',
106 'current_ver': '4.23.0',
107 'latest_ver': '4.24.0',
107 'latest_ver': '4.24.0',
108 },
108 },
109
109
110 'exception': {
110 'exception': {
111 'email_prefix': '[RHODECODE ERROR]',
111 'email_prefix': '[RHODECODE ERROR]',
112 'exc_id': exc_traceback['exc_id'],
112 'exc_id': exc_traceback['exc_id'],
113 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
113 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
114 'exc_type_name': 'NameError',
114 'exc_type_name': 'NameError',
115 'exc_traceback': exc_traceback,
115 'exc_traceback': exc_traceback,
116 },
116 },
117
117
118 'password_reset': {
118 'password_reset': {
119 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
119 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
120
120
121 'user': user,
121 'user': user,
122 'date': datetime.datetime.now(),
122 'date': datetime.datetime.now(),
123 'email': 'test@rhodecode.com',
123 'email': 'test@rhodecode.com',
124 'first_admin_email': User.get_first_super_admin().email
124 'first_admin_email': User.get_first_super_admin().email
125 },
125 },
126
126
127 'password_reset_confirmation': {
127 'password_reset_confirmation': {
128 'new_password': 'new-password-example',
128 'new_password': 'new-password-example',
129 'user': user,
129 'user': user,
130 'date': datetime.datetime.now(),
130 'date': datetime.datetime.now(),
131 'email': 'test@rhodecode.com',
131 'email': 'test@rhodecode.com',
132 'first_admin_email': User.get_first_super_admin().email
132 'first_admin_email': User.get_first_super_admin().email
133 },
133 },
134
134
135 'registration': {
135 'registration': {
136 'user': user,
136 'user': user,
137 'date': datetime.datetime.now(),
137 'date': datetime.datetime.now(),
138 },
138 },
139
139
140 'pull_request_comment': {
140 'pull_request_comment': {
141 'user': user,
141 'user': user,
142
142
143 'status_change': None,
143 'status_change': None,
144 'status_change_type': None,
144 'status_change_type': None,
145
145
146 'pull_request': pr,
146 'pull_request': pr,
147 'pull_request_commits': [],
147 'pull_request_commits': [],
148
148
149 'pull_request_target_repo': target_repo,
149 'pull_request_target_repo': target_repo,
150 'pull_request_target_repo_url': 'http://target-repo/url',
150 'pull_request_target_repo_url': 'http://target-repo/url',
151
151
152 'pull_request_source_repo': source_repo,
152 'pull_request_source_repo': source_repo,
153 'pull_request_source_repo_url': 'http://source-repo/url',
153 'pull_request_source_repo_url': 'http://source-repo/url',
154
154
155 'pull_request_url': 'http://localhost/pr1',
155 'pull_request_url': 'http://localhost/pr1',
156 'pr_comment_url': 'http://comment-url',
156 'pr_comment_url': 'http://comment-url',
157 'pr_comment_reply_url': 'http://comment-url#reply',
157 'pr_comment_reply_url': 'http://comment-url#reply',
158
158
159 'comment_file': None,
159 'comment_file': None,
160 'comment_line': None,
160 'comment_line': None,
161 'comment_type': 'note',
161 'comment_type': 'note',
162 'comment_body': 'This is my comment body. *I like !*',
162 'comment_body': 'This is my comment body. *I like !*',
163 'comment_id': 2048,
163 'comment_id': 2048,
164 'renderer_type': 'markdown',
164 'renderer_type': 'markdown',
165 'mention': True,
165 'mention': True,
166
166
167 },
167 },
168
168
169 'pull_request_comment+status': {
169 'pull_request_comment+status': {
170 'user': user,
170 'user': user,
171
171
172 'status_change': 'approved',
172 'status_change': 'approved',
173 'status_change_type': 'approved',
173 'status_change_type': 'approved',
174
174
175 'pull_request': pr,
175 'pull_request': pr,
176 'pull_request_commits': [],
176 'pull_request_commits': [],
177
177
178 'pull_request_target_repo': target_repo,
178 'pull_request_target_repo': target_repo,
179 'pull_request_target_repo_url': 'http://target-repo/url',
179 'pull_request_target_repo_url': 'http://target-repo/url',
180
180
181 'pull_request_source_repo': source_repo,
181 'pull_request_source_repo': source_repo,
182 'pull_request_source_repo_url': 'http://source-repo/url',
182 'pull_request_source_repo_url': 'http://source-repo/url',
183
183
184 'pull_request_url': 'http://localhost/pr1',
184 'pull_request_url': 'http://localhost/pr1',
185 'pr_comment_url': 'http://comment-url',
185 'pr_comment_url': 'http://comment-url',
186 'pr_comment_reply_url': 'http://comment-url#reply',
186 'pr_comment_reply_url': 'http://comment-url#reply',
187
187
188 'comment_type': 'todo',
188 'comment_type': 'todo',
189 'comment_file': None,
189 'comment_file': None,
190 'comment_line': None,
190 'comment_line': None,
191 'comment_body': '''
191 'comment_body': '''
192 I think something like this would be better
192 I think something like this would be better
193
193
194 ```py
194 ```py
195 // markdown renderer
195 // markdown renderer
196
196
197 def db():
197 def db():
198 global connection
198 global connection
199 return connection
199 return connection
200
200
201 ```
201 ```
202
202
203 ''',
203 ''',
204 'comment_id': 2048,
204 'comment_id': 2048,
205 'renderer_type': 'markdown',
205 'renderer_type': 'markdown',
206 'mention': True,
206 'mention': True,
207
207
208 },
208 },
209
209
210 'pull_request_comment+file': {
210 'pull_request_comment+file': {
211 'user': user,
211 'user': user,
212
212
213 'status_change': None,
213 'status_change': None,
214 'status_change_type': None,
214 'status_change_type': None,
215
215
216 'pull_request': pr,
216 'pull_request': pr,
217 'pull_request_commits': [],
217 'pull_request_commits': [],
218
218
219 'pull_request_target_repo': target_repo,
219 'pull_request_target_repo': target_repo,
220 'pull_request_target_repo_url': 'http://target-repo/url',
220 'pull_request_target_repo_url': 'http://target-repo/url',
221
221
222 'pull_request_source_repo': source_repo,
222 'pull_request_source_repo': source_repo,
223 'pull_request_source_repo_url': 'http://source-repo/url',
223 'pull_request_source_repo_url': 'http://source-repo/url',
224
224
225 'pull_request_url': 'http://localhost/pr1',
225 'pull_request_url': 'http://localhost/pr1',
226
226
227 'pr_comment_url': 'http://comment-url',
227 'pr_comment_url': 'http://comment-url',
228 'pr_comment_reply_url': 'http://comment-url#reply',
228 'pr_comment_reply_url': 'http://comment-url#reply',
229
229
230 'comment_file': 'rhodecode/model/get_flow_commits',
230 'comment_file': 'rhodecode/model/get_flow_commits',
231 'comment_line': 'o1210',
231 'comment_line': 'o1210',
232 'comment_type': 'todo',
232 'comment_type': 'todo',
233 'comment_body': '''
233 'comment_body': '''
234 I like this !
234 I like this !
235
235
236 But please check this code
236 But please check this code
237
237
238 .. code-block:: javascript
238 .. code-block:: javascript
239
239
240 // THIS IS RST CODE
240 // THIS IS RST CODE
241
241
242 this.createResolutionComment = function(commentId) {
242 this.createResolutionComment = function(commentId) {
243 // hide the trigger text
243 // hide the trigger text
244 $('#resolve-comment-{0}'.format(commentId)).hide();
244 $('#resolve-comment-{0}'.format(commentId)).hide();
245
245
246 var comment = $('#comment-'+commentId);
246 var comment = $('#comment-'+commentId);
247 var commentData = comment.data();
247 var commentData = comment.data();
248 if (commentData.commentInline) {
248 if (commentData.commentInline) {
249 this.createComment(comment, f_path, line_no, commentId)
249 this.createComment(comment, f_path, line_no, commentId)
250 } else {
250 } else {
251 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
251 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
252 }
252 }
253
253
254 return false;
254 return false;
255 };
255 };
256
256
257 This should work better !
257 This should work better !
258 ''',
258 ''',
259 'comment_id': 2048,
259 'comment_id': 2048,
260 'renderer_type': 'rst',
260 'renderer_type': 'rst',
261 'mention': True,
261 'mention': True,
262
262
263 },
263 },
264
264
265 'pull_request_update': {
265 'pull_request_update': {
266 'updating_user': user,
266 'updating_user': user,
267
267
268 'status_change': None,
268 'status_change': None,
269 'status_change_type': None,
269 'status_change_type': None,
270
270
271 'pull_request': pr,
271 'pull_request': pr,
272 'pull_request_commits': [],
272 'pull_request_commits': [],
273
273
274 'pull_request_target_repo': target_repo,
274 'pull_request_target_repo': target_repo,
275 'pull_request_target_repo_url': 'http://target-repo/url',
275 'pull_request_target_repo_url': 'http://target-repo/url',
276
276
277 'pull_request_source_repo': source_repo,
277 'pull_request_source_repo': source_repo,
278 'pull_request_source_repo_url': 'http://source-repo/url',
278 'pull_request_source_repo_url': 'http://source-repo/url',
279
279
280 'pull_request_url': 'http://localhost/pr1',
280 'pull_request_url': 'http://localhost/pr1',
281
281
282 # update comment links
282 # update comment links
283 'pr_comment_url': 'http://comment-url',
283 'pr_comment_url': 'http://comment-url',
284 'pr_comment_reply_url': 'http://comment-url#reply',
284 'pr_comment_reply_url': 'http://comment-url#reply',
285 'ancestor_commit_id': 'f39bd443',
285 'ancestor_commit_id': 'f39bd443',
286 'added_commits': commit_changes.added,
286 'added_commits': commit_changes.added,
287 'removed_commits': commit_changes.removed,
287 'removed_commits': commit_changes.removed,
288 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
288 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
289 'added_files': file_changes.added,
289 'added_files': file_changes.added,
290 'modified_files': file_changes.modified,
290 'modified_files': file_changes.modified,
291 'removed_files': file_changes.removed,
291 'removed_files': file_changes.removed,
292 },
292 },
293
293
294 'cs_comment': {
294 'cs_comment': {
295 'user': user,
295 'user': user,
296 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
296 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
297 'status_change': None,
297 'status_change': None,
298 'status_change_type': None,
298 'status_change_type': None,
299
299
300 'commit_target_repo_url': 'http://foo.example.com/#comment1',
300 'commit_target_repo_url': 'http://foo.example.com/#comment1',
301 'repo_name': 'test-repo',
301 'repo_name': 'test-repo',
302 'comment_type': 'note',
302 'comment_type': 'note',
303 'comment_file': None,
303 'comment_file': None,
304 'comment_line': None,
304 'comment_line': None,
305 'commit_comment_url': 'http://comment-url',
305 'commit_comment_url': 'http://comment-url',
306 'commit_comment_reply_url': 'http://comment-url#reply',
306 'commit_comment_reply_url': 'http://comment-url#reply',
307 'comment_body': 'This is my comment body. *I like !*',
307 'comment_body': 'This is my comment body. *I like !*',
308 'comment_id': 2048,
308 'comment_id': 2048,
309 'renderer_type': 'markdown',
309 'renderer_type': 'markdown',
310 'mention': True,
310 'mention': True,
311 },
311 },
312
312
313 'cs_comment+status': {
313 'cs_comment+status': {
314 'user': user,
314 'user': user,
315 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
315 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
316 'status_change': 'approved',
316 'status_change': 'approved',
317 'status_change_type': 'approved',
317 'status_change_type': 'approved',
318
318
319 'commit_target_repo_url': 'http://foo.example.com/#comment1',
319 'commit_target_repo_url': 'http://foo.example.com/#comment1',
320 'repo_name': 'test-repo',
320 'repo_name': 'test-repo',
321 'comment_type': 'note',
321 'comment_type': 'note',
322 'comment_file': None,
322 'comment_file': None,
323 'comment_line': None,
323 'comment_line': None,
324 'commit_comment_url': 'http://comment-url',
324 'commit_comment_url': 'http://comment-url',
325 'commit_comment_reply_url': 'http://comment-url#reply',
325 'commit_comment_reply_url': 'http://comment-url#reply',
326 'comment_body': '''
326 'comment_body': '''
327 Hello **world**
327 Hello **world**
328
328
329 This is a multiline comment :)
329 This is a multiline comment :)
330
330
331 - list
331 - list
332 - list2
332 - list2
333 ''',
333 ''',
334 'comment_id': 2048,
334 'comment_id': 2048,
335 'renderer_type': 'markdown',
335 'renderer_type': 'markdown',
336 'mention': True,
336 'mention': True,
337 },
337 },
338
338
339 'cs_comment+file': {
339 'cs_comment+file': {
340 'user': user,
340 'user': user,
341 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
341 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
342 'status_change': None,
342 'status_change': None,
343 'status_change_type': None,
343 'status_change_type': None,
344
344
345 'commit_target_repo_url': 'http://foo.example.com/#comment1',
345 'commit_target_repo_url': 'http://foo.example.com/#comment1',
346 'repo_name': 'test-repo',
346 'repo_name': 'test-repo',
347
347
348 'comment_type': 'note',
348 'comment_type': 'note',
349 'comment_file': 'test-file.py',
349 'comment_file': 'test-file.py',
350 'comment_line': 'n100',
350 'comment_line': 'n100',
351
351
352 'commit_comment_url': 'http://comment-url',
352 'commit_comment_url': 'http://comment-url',
353 'commit_comment_reply_url': 'http://comment-url#reply',
353 'commit_comment_reply_url': 'http://comment-url#reply',
354 'comment_body': 'This is my comment body. *I like !*',
354 'comment_body': 'This is my comment body. *I like !*',
355 'comment_id': 2048,
355 'comment_id': 2048,
356 'renderer_type': 'markdown',
356 'renderer_type': 'markdown',
357 'mention': True,
357 'mention': True,
358 },
358 },
359
359
360 'pull_request': {
360 'pull_request': {
361 'user': user,
361 'user': user,
362 'pull_request': pr,
362 'pull_request': pr,
363 'pull_request_commits': [
363 'pull_request_commits': [
364 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
364 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
365 my-account: moved email closer to profile as it's similar data just moved outside.
365 my-account: moved email closer to profile as it's similar data just moved outside.
366 '''),
366 '''),
367 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
367 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
368 users: description edit fixes
368 users: description edit fixes
369
369
370 - tests
370 - tests
371 - added metatags info
371 - added metatags info
372 '''),
372 '''),
373 ],
373 ],
374
374
375 'pull_request_target_repo': target_repo,
375 'pull_request_target_repo': target_repo,
376 'pull_request_target_repo_url': 'http://target-repo/url',
376 'pull_request_target_repo_url': 'http://target-repo/url',
377
377
378 'pull_request_source_repo': source_repo,
378 'pull_request_source_repo': source_repo,
379 'pull_request_source_repo_url': 'http://source-repo/url',
379 'pull_request_source_repo_url': 'http://source-repo/url',
380
380
381 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
381 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
382 'user_role': 'reviewer',
382 'user_role': 'reviewer',
383 },
383 },
384
384
385 'pull_request+reviewer_role': {
385 'pull_request+reviewer_role': {
386 'user': user,
386 'user': user,
387 'pull_request': pr,
387 'pull_request': pr,
388 'pull_request_commits': [
388 'pull_request_commits': [
389 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
389 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
390 my-account: moved email closer to profile as it's similar data just moved outside.
390 my-account: moved email closer to profile as it's similar data just moved outside.
391 '''),
391 '''),
392 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
392 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
393 users: description edit fixes
393 users: description edit fixes
394
394
395 - tests
395 - tests
396 - added metatags info
396 - added metatags info
397 '''),
397 '''),
398 ],
398 ],
399
399
400 'pull_request_target_repo': target_repo,
400 'pull_request_target_repo': target_repo,
401 'pull_request_target_repo_url': 'http://target-repo/url',
401 'pull_request_target_repo_url': 'http://target-repo/url',
402
402
403 'pull_request_source_repo': source_repo,
403 'pull_request_source_repo': source_repo,
404 'pull_request_source_repo_url': 'http://source-repo/url',
404 'pull_request_source_repo_url': 'http://source-repo/url',
405
405
406 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
406 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
407 'user_role': 'reviewer',
407 'user_role': 'reviewer',
408 },
408 },
409
409
410 'pull_request+observer_role': {
410 'pull_request+observer_role': {
411 'user': user,
411 'user': user,
412 'pull_request': pr,
412 'pull_request': pr,
413 'pull_request_commits': [
413 'pull_request_commits': [
414 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
414 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
415 my-account: moved email closer to profile as it's similar data just moved outside.
415 my-account: moved email closer to profile as it's similar data just moved outside.
416 '''),
416 '''),
417 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
417 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
418 users: description edit fixes
418 users: description edit fixes
419
419
420 - tests
420 - tests
421 - added metatags info
421 - added metatags info
422 '''),
422 '''),
423 ],
423 ],
424
424
425 'pull_request_target_repo': target_repo,
425 'pull_request_target_repo': target_repo,
426 'pull_request_target_repo_url': 'http://target-repo/url',
426 'pull_request_target_repo_url': 'http://target-repo/url',
427
427
428 'pull_request_source_repo': source_repo,
428 'pull_request_source_repo': source_repo,
429 'pull_request_source_repo_url': 'http://source-repo/url',
429 'pull_request_source_repo_url': 'http://source-repo/url',
430
430
431 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
431 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
432 'user_role': 'observer'
432 'user_role': 'observer'
433 }
433 }
434 }
434 }
435
435
436 template_type = email_id.split('+')[0]
436 template_type = email_id.split('+')[0]
437 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
437 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
438 template_type, **email_kwargs.get(email_id, {}))
438 template_type, **email_kwargs.get(email_id, {}))
439
439
440 test_email = self.request.GET.get('email')
440 test_email = self.request.GET.get('email')
441 if test_email:
441 if test_email:
442 recipients = [test_email]
442 recipients = [test_email]
443 run_task(tasks.send_email, recipients, c.subject,
443 run_task(tasks.send_email, recipients, c.subject,
444 c.email_body_plaintext, c.email_body)
444 c.email_body_plaintext, c.email_body)
445
445
446 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
446 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
447 template = 'debug_style/email_plain_rendered.mako'
447 template = 'debug_style/email_plain_rendered.mako'
448 else:
448 else:
449 template = 'debug_style/email.mako'
449 template = 'debug_style/email.mako'
450 return render_to_response(
450 return render_to_response(
451 template, self._get_template_context(c),
451 template, self._get_template_context(c),
452 request=self.request)
452 request=self.request)
453
453
454 def template(self):
454 def template(self):
455 t_path = self.request.matchdict['t_path']
455 t_path = self.request.matchdict['t_path']
456 c = self.load_default_context()
456 c = self.load_default_context()
457 c.active = os.path.splitext(t_path)[0]
457 c.active = os.path.splitext(t_path)[0]
458 c.came_from = ''
458 c.came_from = ''
459 # NOTE(marcink): extend the email types with variations based on data sets
459 # NOTE(marcink): extend the email types with variations based on data sets
460 c.email_types = {
460 c.email_types = {
461 'cs_comment+file': {},
461 'cs_comment+file': {},
462 'cs_comment+status': {},
462 'cs_comment+status': {},
463
463
464 'pull_request_comment+file': {},
464 'pull_request_comment+file': {},
465 'pull_request_comment+status': {},
465 'pull_request_comment+status': {},
466
466
467 'pull_request_update': {},
467 'pull_request_update': {},
468
468
469 'pull_request+reviewer_role': {},
469 'pull_request+reviewer_role': {},
470 'pull_request+observer_role': {},
470 'pull_request+observer_role': {},
471 }
471 }
472 c.email_types.update(EmailNotificationModel.email_types)
472 c.email_types.update(EmailNotificationModel.email_types)
473
473
474 return render_to_response(
474 return render_to_response(
475 'debug_style/' + t_path, self._get_template_context(c),
475 'debug_style/' + t_path, self._get_template_context(c),
476 request=self.request)
476 request=self.request)
@@ -1,68 +1,68 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
20 import os
21 from rhodecode.apps.file_store import config_keys
21 from rhodecode.apps.file_store import config_keys
22 from rhodecode.config.settings_maker import SettingsMaker
22 from rhodecode.config.settings_maker import SettingsMaker
23
23
24
24
25 def _sanitize_settings_and_apply_defaults(settings):
25 def _sanitize_settings_and_apply_defaults(settings):
26 """
26 """
27 Set defaults, convert to python types and validate settings.
27 Set defaults, convert to python types and validate settings.
28 """
28 """
29 settings_maker = SettingsMaker(settings)
29 settings_maker = SettingsMaker(settings)
30
30
31 settings_maker.make_setting(config_keys.enabled, True, parser='bool')
31 settings_maker.make_setting(config_keys.enabled, True, parser='bool')
32 settings_maker.make_setting(config_keys.backend, 'local')
32 settings_maker.make_setting(config_keys.backend, 'local')
33
33
34 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
34 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
35 settings_maker.make_setting(config_keys.store_path, default_store)
35 settings_maker.make_setting(config_keys.store_path, default_store)
36
36
37 settings_maker.env_expand()
37 settings_maker.env_expand()
38
38
39
39
40 def includeme(config):
40 def includeme(config):
41 from rhodecode.apps.file_store.views import FileStoreView
41 from rhodecode.apps.file_store.views import FileStoreView
42
42
43 settings = config.registry.settings
43 settings = config.registry.settings
44 _sanitize_settings_and_apply_defaults(settings)
44 _sanitize_settings_and_apply_defaults(settings)
45
45
46 config.add_route(
46 config.add_route(
47 name='upload_file',
47 name='upload_file',
48 pattern='/_file_store/upload')
48 pattern='/_file_store/upload')
49 config.add_view(
49 config.add_view(
50 FileStoreView,
50 FileStoreView,
51 attr='upload_file',
51 attr='upload_file',
52 route_name='upload_file', request_method='POST', renderer='json_ext')
52 route_name='upload_file', request_method='POST', renderer='json_ext')
53
53
54 config.add_route(
54 config.add_route(
55 name='download_file',
55 name='download_file',
56 pattern='/_file_store/download/{fid:.*}')
56 pattern='/_file_store/download/{fid:.*}')
57 config.add_view(
57 config.add_view(
58 FileStoreView,
58 FileStoreView,
59 attr='download_file',
59 attr='download_file',
60 route_name='download_file')
60 route_name='download_file')
61
61
62 config.add_route(
62 config.add_route(
63 name='download_file_by_token',
63 name='download_file_by_token',
64 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
64 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
65 config.add_view(
65 config.add_view(
66 FileStoreView,
66 FileStoreView,
67 attr='download_file_by_token',
67 attr='download_file_by_token',
68 route_name='download_file_by_token')
68 route_name='download_file_by_token')
@@ -1,19 +1,19 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -1,270 +1,270 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import time
22 import time
23 import errno
23 import errno
24 import hashlib
24 import hashlib
25
25
26 from rhodecode.lib.ext_json import json
26 from rhodecode.lib.ext_json import json
27 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.extensions import resolve_extensions
28 from rhodecode.apps.file_store.extensions import resolve_extensions
29 from rhodecode.apps.file_store.exceptions import (
29 from rhodecode.apps.file_store.exceptions import (
30 FileNotAllowedException, FileOverSizeException)
30 FileNotAllowedException, FileOverSizeException)
31
31
32 METADATA_VER = 'v1'
32 METADATA_VER = 'v1'
33
33
34
34
35 def safe_make_dirs(dir_path):
35 def safe_make_dirs(dir_path):
36 if not os.path.exists(dir_path):
36 if not os.path.exists(dir_path):
37 try:
37 try:
38 os.makedirs(dir_path)
38 os.makedirs(dir_path)
39 except OSError as e:
39 except OSError as e:
40 if e.errno != errno.EEXIST:
40 if e.errno != errno.EEXIST:
41 raise
41 raise
42 return
42 return
43
43
44
44
45 class LocalFileStorage(object):
45 class LocalFileStorage(object):
46
46
47 @classmethod
47 @classmethod
48 def apply_counter(cls, counter, filename):
48 def apply_counter(cls, counter, filename):
49 name_counted = '%d-%s' % (counter, filename)
49 name_counted = '%d-%s' % (counter, filename)
50 return name_counted
50 return name_counted
51
51
52 @classmethod
52 @classmethod
53 def resolve_name(cls, name, directory):
53 def resolve_name(cls, name, directory):
54 """
54 """
55 Resolves a unique name and the correct path. If a filename
55 Resolves a unique name and the correct path. If a filename
56 for that path already exists then a numeric prefix with values > 0 will be
56 for that path already exists then a numeric prefix with values > 0 will be
57 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
57 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
58
58
59 :param name: base name of file
59 :param name: base name of file
60 :param directory: absolute directory path
60 :param directory: absolute directory path
61 """
61 """
62
62
63 counter = 0
63 counter = 0
64 while True:
64 while True:
65 name_counted = cls.apply_counter(counter, name)
65 name_counted = cls.apply_counter(counter, name)
66
66
67 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
67 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
68 sub_store = cls._sub_store_from_filename(name_counted)
68 sub_store = cls._sub_store_from_filename(name_counted)
69 sub_store_path = os.path.join(directory, sub_store)
69 sub_store_path = os.path.join(directory, sub_store)
70 safe_make_dirs(sub_store_path)
70 safe_make_dirs(sub_store_path)
71
71
72 path = os.path.join(sub_store_path, name_counted)
72 path = os.path.join(sub_store_path, name_counted)
73 if not os.path.exists(path):
73 if not os.path.exists(path):
74 return name_counted, path
74 return name_counted, path
75 counter += 1
75 counter += 1
76
76
77 @classmethod
77 @classmethod
78 def _sub_store_from_filename(cls, filename):
78 def _sub_store_from_filename(cls, filename):
79 return filename[:2]
79 return filename[:2]
80
80
81 @classmethod
81 @classmethod
82 def calculate_path_hash(cls, file_path):
82 def calculate_path_hash(cls, file_path):
83 """
83 """
84 Efficient calculation of file_path sha256 sum
84 Efficient calculation of file_path sha256 sum
85
85
86 :param file_path:
86 :param file_path:
87 :return: sha256sum
87 :return: sha256sum
88 """
88 """
89 digest = hashlib.sha256()
89 digest = hashlib.sha256()
90 with open(file_path, 'rb') as f:
90 with open(file_path, 'rb') as f:
91 for chunk in iter(lambda: f.read(1024 * 100), b""):
91 for chunk in iter(lambda: f.read(1024 * 100), b""):
92 digest.update(chunk)
92 digest.update(chunk)
93
93
94 return digest.hexdigest()
94 return digest.hexdigest()
95
95
96 def __init__(self, base_path, extension_groups=None):
96 def __init__(self, base_path, extension_groups=None):
97
97
98 """
98 """
99 Local file storage
99 Local file storage
100
100
101 :param base_path: the absolute base path where uploads are stored
101 :param base_path: the absolute base path where uploads are stored
102 :param extension_groups: extensions string
102 :param extension_groups: extensions string
103 """
103 """
104
104
105 extension_groups = extension_groups or ['any']
105 extension_groups = extension_groups or ['any']
106 self.base_path = base_path
106 self.base_path = base_path
107 self.extensions = resolve_extensions([], groups=extension_groups)
107 self.extensions = resolve_extensions([], groups=extension_groups)
108
108
109 def __repr__(self):
109 def __repr__(self):
110 return '{}@{}'.format(self.__class__, self.base_path)
110 return '{}@{}'.format(self.__class__, self.base_path)
111
111
112 def store_path(self, filename):
112 def store_path(self, filename):
113 """
113 """
114 Returns absolute file path of the filename, joined to the
114 Returns absolute file path of the filename, joined to the
115 base_path.
115 base_path.
116
116
117 :param filename: base name of file
117 :param filename: base name of file
118 """
118 """
119 prefix_dir = ''
119 prefix_dir = ''
120 if '/' in filename:
120 if '/' in filename:
121 prefix_dir, filename = filename.split('/')
121 prefix_dir, filename = filename.split('/')
122 sub_store = self._sub_store_from_filename(filename)
122 sub_store = self._sub_store_from_filename(filename)
123 else:
123 else:
124 sub_store = self._sub_store_from_filename(filename)
124 sub_store = self._sub_store_from_filename(filename)
125 return os.path.join(self.base_path, prefix_dir, sub_store, filename)
125 return os.path.join(self.base_path, prefix_dir, sub_store, filename)
126
126
127 def delete(self, filename):
127 def delete(self, filename):
128 """
128 """
129 Deletes the filename. Filename is resolved with the
129 Deletes the filename. Filename is resolved with the
130 absolute path based on base_path. If file does not exist,
130 absolute path based on base_path. If file does not exist,
131 returns **False**, otherwise **True**
131 returns **False**, otherwise **True**
132
132
133 :param filename: base name of file
133 :param filename: base name of file
134 """
134 """
135 if self.exists(filename):
135 if self.exists(filename):
136 os.remove(self.store_path(filename))
136 os.remove(self.store_path(filename))
137 return True
137 return True
138 return False
138 return False
139
139
140 def exists(self, filename):
140 def exists(self, filename):
141 """
141 """
142 Checks if file exists. Resolves filename's absolute
142 Checks if file exists. Resolves filename's absolute
143 path based on base_path.
143 path based on base_path.
144
144
145 :param filename: file_uid name of file, e.g 0-f62b2b2d-9708-4079-a071-ec3f958448d4.svg
145 :param filename: file_uid name of file, e.g 0-f62b2b2d-9708-4079-a071-ec3f958448d4.svg
146 """
146 """
147 return os.path.exists(self.store_path(filename))
147 return os.path.exists(self.store_path(filename))
148
148
149 def filename_allowed(self, filename, extensions=None):
149 def filename_allowed(self, filename, extensions=None):
150 """Checks if a filename has an allowed extension
150 """Checks if a filename has an allowed extension
151
151
152 :param filename: base name of file
152 :param filename: base name of file
153 :param extensions: iterable of extensions (or self.extensions)
153 :param extensions: iterable of extensions (or self.extensions)
154 """
154 """
155 _, ext = os.path.splitext(filename)
155 _, ext = os.path.splitext(filename)
156 return self.extension_allowed(ext, extensions)
156 return self.extension_allowed(ext, extensions)
157
157
158 def extension_allowed(self, ext, extensions=None):
158 def extension_allowed(self, ext, extensions=None):
159 """
159 """
160 Checks if an extension is permitted. Both e.g. ".jpg" and
160 Checks if an extension is permitted. Both e.g. ".jpg" and
161 "jpg" can be passed in. Extension lookup is case-insensitive.
161 "jpg" can be passed in. Extension lookup is case-insensitive.
162
162
163 :param ext: extension to check
163 :param ext: extension to check
164 :param extensions: iterable of extensions to validate against (or self.extensions)
164 :param extensions: iterable of extensions to validate against (or self.extensions)
165 """
165 """
166 def normalize_ext(_ext):
166 def normalize_ext(_ext):
167 if _ext.startswith('.'):
167 if _ext.startswith('.'):
168 _ext = _ext[1:]
168 _ext = _ext[1:]
169 return _ext.lower()
169 return _ext.lower()
170
170
171 extensions = extensions or self.extensions
171 extensions = extensions or self.extensions
172 if not extensions:
172 if not extensions:
173 return True
173 return True
174
174
175 ext = normalize_ext(ext)
175 ext = normalize_ext(ext)
176
176
177 return ext in [normalize_ext(x) for x in extensions]
177 return ext in [normalize_ext(x) for x in extensions]
178
178
179 def save_file(self, file_obj, filename, directory=None, extensions=None,
179 def save_file(self, file_obj, filename, directory=None, extensions=None,
180 extra_metadata=None, max_filesize=None, randomized_name=True, **kwargs):
180 extra_metadata=None, max_filesize=None, randomized_name=True, **kwargs):
181 """
181 """
182 Saves a file object to the uploads location.
182 Saves a file object to the uploads location.
183 Returns the resolved filename, i.e. the directory +
183 Returns the resolved filename, i.e. the directory +
184 the (randomized/incremented) base name.
184 the (randomized/incremented) base name.
185
185
186 :param file_obj: **cgi.FieldStorage** object (or similar)
186 :param file_obj: **cgi.FieldStorage** object (or similar)
187 :param filename: original filename
187 :param filename: original filename
188 :param directory: relative path of sub-directory
188 :param directory: relative path of sub-directory
189 :param extensions: iterable of allowed extensions, if not default
189 :param extensions: iterable of allowed extensions, if not default
190 :param max_filesize: maximum size of file that should be allowed
190 :param max_filesize: maximum size of file that should be allowed
191 :param randomized_name: generate random generated UID or fixed based on the filename
191 :param randomized_name: generate random generated UID or fixed based on the filename
192 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
192 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
193
193
194 """
194 """
195
195
196 extensions = extensions or self.extensions
196 extensions = extensions or self.extensions
197
197
198 if not self.filename_allowed(filename, extensions):
198 if not self.filename_allowed(filename, extensions):
199 raise FileNotAllowedException()
199 raise FileNotAllowedException()
200
200
201 if directory:
201 if directory:
202 dest_directory = os.path.join(self.base_path, directory)
202 dest_directory = os.path.join(self.base_path, directory)
203 else:
203 else:
204 dest_directory = self.base_path
204 dest_directory = self.base_path
205
205
206 safe_make_dirs(dest_directory)
206 safe_make_dirs(dest_directory)
207
207
208 uid_filename = utils.uid_filename(filename, randomized=randomized_name)
208 uid_filename = utils.uid_filename(filename, randomized=randomized_name)
209
209
210 # resolve also produces special sub-dir for file optimized store
210 # resolve also produces special sub-dir for file optimized store
211 filename, path = self.resolve_name(uid_filename, dest_directory)
211 filename, path = self.resolve_name(uid_filename, dest_directory)
212 stored_file_dir = os.path.dirname(path)
212 stored_file_dir = os.path.dirname(path)
213
213
214 no_body_seek = kwargs.pop('no_body_seek', False)
214 no_body_seek = kwargs.pop('no_body_seek', False)
215 if no_body_seek:
215 if no_body_seek:
216 pass
216 pass
217 else:
217 else:
218 file_obj.seek(0)
218 file_obj.seek(0)
219
219
220 with open(path, "wb") as dest:
220 with open(path, "wb") as dest:
221 length = 256 * 1024
221 length = 256 * 1024
222 while 1:
222 while 1:
223 buf = file_obj.read(length)
223 buf = file_obj.read(length)
224 if not buf:
224 if not buf:
225 break
225 break
226 dest.write(buf)
226 dest.write(buf)
227
227
228 metadata = {}
228 metadata = {}
229 if extra_metadata:
229 if extra_metadata:
230 metadata = extra_metadata
230 metadata = extra_metadata
231
231
232 size = os.stat(path).st_size
232 size = os.stat(path).st_size
233
233
234 if max_filesize and size > max_filesize:
234 if max_filesize and size > max_filesize:
235 # free up the copied file, and raise exc
235 # free up the copied file, and raise exc
236 os.remove(path)
236 os.remove(path)
237 raise FileOverSizeException()
237 raise FileOverSizeException()
238
238
239 file_hash = self.calculate_path_hash(path)
239 file_hash = self.calculate_path_hash(path)
240
240
241 metadata.update({
241 metadata.update({
242 "filename": filename,
242 "filename": filename,
243 "size": size,
243 "size": size,
244 "time": time.time(),
244 "time": time.time(),
245 "sha256": file_hash,
245 "sha256": file_hash,
246 "meta_ver": METADATA_VER
246 "meta_ver": METADATA_VER
247 })
247 })
248
248
249 filename_meta = filename + '.meta'
249 filename_meta = filename + '.meta'
250 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
250 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
251 dest_meta.write(json.dumps(metadata))
251 dest_meta.write(json.dumps(metadata))
252
252
253 if directory:
253 if directory:
254 filename = os.path.join(directory, filename)
254 filename = os.path.join(directory, filename)
255
255
256 return filename, metadata
256 return filename, metadata
257
257
258 def get_metadata(self, filename, ignore_missing=False):
258 def get_metadata(self, filename, ignore_missing=False):
259 """
259 """
260 Reads JSON stored metadata for a file
260 Reads JSON stored metadata for a file
261
261
262 :param filename:
262 :param filename:
263 :return:
263 :return:
264 """
264 """
265 filename = self.store_path(filename)
265 filename = self.store_path(filename)
266 filename_meta = filename + '.meta'
266 filename_meta = filename + '.meta'
267 if ignore_missing and not os.path.isfile(filename_meta):
267 if ignore_missing and not os.path.isfile(filename_meta):
268 return {}
268 return {}
269 with open(filename_meta, "rb") as source_meta:
269 with open(filename_meta, "rb") as source_meta:
270 return json.loads(source_meta.read())
270 return json.loads(source_meta.read())
@@ -1,27 +1,27 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 # Definition of setting keys used to configure this module. Defined here to
22 # Definition of setting keys used to configure this module. Defined here to
23 # avoid repetition of keys throughout the module.
23 # avoid repetition of keys throughout the module.
24
24
25 enabled = 'file_store.enabled'
25 enabled = 'file_store.enabled'
26 backend = 'file_store.backend'
26 backend = 'file_store.backend'
27 store_path = 'file_store.storage_path'
27 store_path = 'file_store.storage_path'
@@ -1,31 +1,31 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 class FileNotAllowedException(Exception):
22 class FileNotAllowedException(Exception):
23 """
23 """
24 Thrown if file does not have an allowed extension.
24 Thrown if file does not have an allowed extension.
25 """
25 """
26
26
27
27
28 class FileOverSizeException(Exception):
28 class FileOverSizeException(Exception):
29 """
29 """
30 Thrown if file is over the set limit.
30 Thrown if file is over the set limit.
31 """
31 """
@@ -1,66 +1,66 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 ANY = []
22 ANY = []
23 TEXT_EXT = ['txt', 'md', 'rst', 'log']
23 TEXT_EXT = ['txt', 'md', 'rst', 'log']
24 DOCUMENTS_EXT = ['pdf', 'rtf', 'odf', 'ods', 'gnumeric', 'abw', 'doc', 'docx', 'xls', 'xlsx']
24 DOCUMENTS_EXT = ['pdf', 'rtf', 'odf', 'ods', 'gnumeric', 'abw', 'doc', 'docx', 'xls', 'xlsx']
25 IMAGES_EXT = ['jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tiff']
25 IMAGES_EXT = ['jpg', 'jpe', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tiff']
26 AUDIO_EXT = ['wav', 'mp3', 'aac', 'ogg', 'oga', 'flac']
26 AUDIO_EXT = ['wav', 'mp3', 'aac', 'ogg', 'oga', 'flac']
27 VIDEO_EXT = ['mpeg', '3gp', 'avi', 'divx', 'dvr', 'flv', 'mp4', 'wmv']
27 VIDEO_EXT = ['mpeg', '3gp', 'avi', 'divx', 'dvr', 'flv', 'mp4', 'wmv']
28 DATA_EXT = ['csv', 'ini', 'json', 'plist', 'xml', 'yaml', 'yml']
28 DATA_EXT = ['csv', 'ini', 'json', 'plist', 'xml', 'yaml', 'yml']
29 SCRIPTS_EXT = ['js', 'php', 'pl', 'py', 'rb', 'sh', 'go', 'c', 'h']
29 SCRIPTS_EXT = ['js', 'php', 'pl', 'py', 'rb', 'sh', 'go', 'c', 'h']
30 ARCHIVES_EXT = ['gz', 'bz2', 'zip', 'tar', 'tgz', 'txz', '7z']
30 ARCHIVES_EXT = ['gz', 'bz2', 'zip', 'tar', 'tgz', 'txz', '7z']
31 EXECUTABLES_EXT = ['so', 'exe', 'dll']
31 EXECUTABLES_EXT = ['so', 'exe', 'dll']
32
32
33
33
34 DEFAULT = DOCUMENTS_EXT + TEXT_EXT + IMAGES_EXT + DATA_EXT
34 DEFAULT = DOCUMENTS_EXT + TEXT_EXT + IMAGES_EXT + DATA_EXT
35
35
36 GROUPS = dict((
36 GROUPS = dict((
37 ('any', ANY),
37 ('any', ANY),
38 ('text', TEXT_EXT),
38 ('text', TEXT_EXT),
39 ('documents', DOCUMENTS_EXT),
39 ('documents', DOCUMENTS_EXT),
40 ('images', IMAGES_EXT),
40 ('images', IMAGES_EXT),
41 ('audio', AUDIO_EXT),
41 ('audio', AUDIO_EXT),
42 ('video', VIDEO_EXT),
42 ('video', VIDEO_EXT),
43 ('data', DATA_EXT),
43 ('data', DATA_EXT),
44 ('scripts', SCRIPTS_EXT),
44 ('scripts', SCRIPTS_EXT),
45 ('archives', ARCHIVES_EXT),
45 ('archives', ARCHIVES_EXT),
46 ('executables', EXECUTABLES_EXT),
46 ('executables', EXECUTABLES_EXT),
47 ('default', DEFAULT),
47 ('default', DEFAULT),
48 ))
48 ))
49
49
50
50
51 def resolve_extensions(extensions, groups=None):
51 def resolve_extensions(extensions, groups=None):
52 """
52 """
53 Calculate allowed extensions based on a list of extensions provided, and optional
53 Calculate allowed extensions based on a list of extensions provided, and optional
54 groups of extensions from the available lists.
54 groups of extensions from the available lists.
55
55
56 :param extensions: a list of extensions e.g ['py', 'txt']
56 :param extensions: a list of extensions e.g ['py', 'txt']
57 :param groups: additionally groups to extend the extensions.
57 :param groups: additionally groups to extend the extensions.
58 """
58 """
59 groups = groups or []
59 groups = groups or []
60 valid_exts = set([x.lower() for x in extensions])
60 valid_exts = set([x.lower() for x in extensions])
61
61
62 for group in groups:
62 for group in groups:
63 if group in GROUPS:
63 if group in GROUPS:
64 valid_exts.update(GROUPS[group])
64 valid_exts.update(GROUPS[group])
65
65
66 return valid_exts
66 return valid_exts
@@ -1,20 +1,20 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
@@ -1,261 +1,260 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import os
19 import os
21 import pytest
20 import pytest
22
21
23 from rhodecode.lib.ext_json import json
22 from rhodecode.lib.ext_json import json
24 from rhodecode.model.auth_token import AuthTokenModel
23 from rhodecode.model.auth_token import AuthTokenModel
25 from rhodecode.model.db import Session, FileStore, Repository, User
24 from rhodecode.model.db import Session, FileStore, Repository, User
26 from rhodecode.tests import TestController
25 from rhodecode.tests import TestController
27 from rhodecode.apps.file_store import utils, config_keys
26 from rhodecode.apps.file_store import utils, config_keys
28
27
29
28
30 def route_path(name, params=None, **kwargs):
29 def route_path(name, params=None, **kwargs):
31 import urllib.request, urllib.parse, urllib.error
30 import urllib.request, urllib.parse, urllib.error
32
31
33 base_url = {
32 base_url = {
34 'upload_file': '/_file_store/upload',
33 'upload_file': '/_file_store/upload',
35 'download_file': '/_file_store/download/{fid}',
34 'download_file': '/_file_store/download/{fid}',
36 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
35 'download_file_by_token': '/_file_store/token-download/{_auth_token}/{fid}'
37
36
38 }[name].format(**kwargs)
37 }[name].format(**kwargs)
39
38
40 if params:
39 if params:
41 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
40 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
42 return base_url
41 return base_url
43
42
44
43
45 class TestFileStoreViews(TestController):
44 class TestFileStoreViews(TestController):
46
45
47 @pytest.mark.parametrize("fid, content, exists", [
46 @pytest.mark.parametrize("fid, content, exists", [
48 ('abcde-0.jpg', "xxxxx", True),
47 ('abcde-0.jpg', "xxxxx", True),
49 ('abcde-0.exe', "1234567", True),
48 ('abcde-0.exe', "1234567", True),
50 ('abcde-0.jpg', "xxxxx", False),
49 ('abcde-0.jpg', "xxxxx", False),
51 ])
50 ])
52 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
51 def test_get_files_from_store(self, fid, content, exists, tmpdir, user_util):
53 user = self.log_user()
52 user = self.log_user()
54 user_id = user['user_id']
53 user_id = user['user_id']
55 repo_id = user_util.create_repo().repo_id
54 repo_id = user_util.create_repo().repo_id
56 store_path = self.app._pyramid_settings[config_keys.store_path]
55 store_path = self.app._pyramid_settings[config_keys.store_path]
57 store_uid = fid
56 store_uid = fid
58
57
59 if exists:
58 if exists:
60 status = 200
59 status = 200
61 store = utils.get_file_storage({config_keys.store_path: store_path})
60 store = utils.get_file_storage({config_keys.store_path: store_path})
62 filesystem_file = os.path.join(str(tmpdir), fid)
61 filesystem_file = os.path.join(str(tmpdir), fid)
63 with open(filesystem_file, 'wb') as f:
62 with open(filesystem_file, 'wb') as f:
64 f.write(content)
63 f.write(content)
65
64
66 with open(filesystem_file, 'rb') as f:
65 with open(filesystem_file, 'rb') as f:
67 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
66 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
68
67
69 entry = FileStore.create(
68 entry = FileStore.create(
70 file_uid=store_uid, filename=metadata["filename"],
69 file_uid=store_uid, filename=metadata["filename"],
71 file_hash=metadata["sha256"], file_size=metadata["size"],
70 file_hash=metadata["sha256"], file_size=metadata["size"],
72 file_display_name='file_display_name',
71 file_display_name='file_display_name',
73 file_description='repo artifact `{}`'.format(metadata["filename"]),
72 file_description='repo artifact `{}`'.format(metadata["filename"]),
74 check_acl=True, user_id=user_id,
73 check_acl=True, user_id=user_id,
75 scope_repo_id=repo_id
74 scope_repo_id=repo_id
76 )
75 )
77 Session().add(entry)
76 Session().add(entry)
78 Session().commit()
77 Session().commit()
79
78
80 else:
79 else:
81 status = 404
80 status = 404
82
81
83 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
82 response = self.app.get(route_path('download_file', fid=store_uid), status=status)
84
83
85 if exists:
84 if exists:
86 assert response.text == content
85 assert response.text == content
87 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
86 file_store_path = os.path.dirname(store.resolve_name(store_uid, store_path)[1])
88 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
87 metadata_file = os.path.join(file_store_path, store_uid + '.meta')
89 assert os.path.exists(metadata_file)
88 assert os.path.exists(metadata_file)
90 with open(metadata_file, 'rb') as f:
89 with open(metadata_file, 'rb') as f:
91 json_data = json.loads(f.read())
90 json_data = json.loads(f.read())
92
91
93 assert json_data
92 assert json_data
94 assert 'size' in json_data
93 assert 'size' in json_data
95
94
96 def test_upload_files_without_content_to_store(self):
95 def test_upload_files_without_content_to_store(self):
97 self.log_user()
96 self.log_user()
98 response = self.app.post(
97 response = self.app.post(
99 route_path('upload_file'),
98 route_path('upload_file'),
100 params={'csrf_token': self.csrf_token},
99 params={'csrf_token': self.csrf_token},
101 status=200)
100 status=200)
102
101
103 assert response.json == {
102 assert response.json == {
104 u'error': u'store_file data field is missing',
103 u'error': u'store_file data field is missing',
105 u'access_path': None,
104 u'access_path': None,
106 u'store_fid': None}
105 u'store_fid': None}
107
106
108 def test_upload_files_bogus_content_to_store(self):
107 def test_upload_files_bogus_content_to_store(self):
109 self.log_user()
108 self.log_user()
110 response = self.app.post(
109 response = self.app.post(
111 route_path('upload_file'),
110 route_path('upload_file'),
112 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
111 params={'csrf_token': self.csrf_token, 'store_file': 'bogus'},
113 status=200)
112 status=200)
114
113
115 assert response.json == {
114 assert response.json == {
116 u'error': u'filename cannot be read from the data field',
115 u'error': u'filename cannot be read from the data field',
117 u'access_path': None,
116 u'access_path': None,
118 u'store_fid': None}
117 u'store_fid': None}
119
118
120 def test_upload_content_to_store(self):
119 def test_upload_content_to_store(self):
121 self.log_user()
120 self.log_user()
122 response = self.app.post(
121 response = self.app.post(
123 route_path('upload_file'),
122 route_path('upload_file'),
124 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
123 upload_files=[('store_file', 'myfile.txt', 'SOME CONTENT')],
125 params={'csrf_token': self.csrf_token},
124 params={'csrf_token': self.csrf_token},
126 status=200)
125 status=200)
127
126
128 assert response.json['store_fid']
127 assert response.json['store_fid']
129
128
130 @pytest.fixture()
129 @pytest.fixture()
131 def create_artifact_factory(self, tmpdir):
130 def create_artifact_factory(self, tmpdir):
132 def factory(user_id, content):
131 def factory(user_id, content):
133 store_path = self.app._pyramid_settings[config_keys.store_path]
132 store_path = self.app._pyramid_settings[config_keys.store_path]
134 store = utils.get_file_storage({config_keys.store_path: store_path})
133 store = utils.get_file_storage({config_keys.store_path: store_path})
135 fid = 'example.txt'
134 fid = 'example.txt'
136
135
137 filesystem_file = os.path.join(str(tmpdir), fid)
136 filesystem_file = os.path.join(str(tmpdir), fid)
138 with open(filesystem_file, 'wb') as f:
137 with open(filesystem_file, 'wb') as f:
139 f.write(content)
138 f.write(content)
140
139
141 with open(filesystem_file, 'rb') as f:
140 with open(filesystem_file, 'rb') as f:
142 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
141 store_uid, metadata = store.save_file(f, fid, extra_metadata={'filename': fid})
143
142
144 entry = FileStore.create(
143 entry = FileStore.create(
145 file_uid=store_uid, filename=metadata["filename"],
144 file_uid=store_uid, filename=metadata["filename"],
146 file_hash=metadata["sha256"], file_size=metadata["size"],
145 file_hash=metadata["sha256"], file_size=metadata["size"],
147 file_display_name='file_display_name',
146 file_display_name='file_display_name',
148 file_description='repo artifact `{}`'.format(metadata["filename"]),
147 file_description='repo artifact `{}`'.format(metadata["filename"]),
149 check_acl=True, user_id=user_id,
148 check_acl=True, user_id=user_id,
150 )
149 )
151 Session().add(entry)
150 Session().add(entry)
152 Session().commit()
151 Session().commit()
153 return entry
152 return entry
154 return factory
153 return factory
155
154
156 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
155 def test_download_file_non_scoped(self, user_util, create_artifact_factory):
157 user = self.log_user()
156 user = self.log_user()
158 user_id = user['user_id']
157 user_id = user['user_id']
159 content = 'HELLO MY NAME IS ARTIFACT !'
158 content = 'HELLO MY NAME IS ARTIFACT !'
160
159
161 artifact = create_artifact_factory(user_id, content)
160 artifact = create_artifact_factory(user_id, content)
162 file_uid = artifact.file_uid
161 file_uid = artifact.file_uid
163 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
162 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
164 assert response.text == content
163 assert response.text == content
165
164
166 # log-in to new user and test download again
165 # log-in to new user and test download again
167 user = user_util.create_user(password='qweqwe')
166 user = user_util.create_user(password='qweqwe')
168 self.log_user(user.username, 'qweqwe')
167 self.log_user(user.username, 'qweqwe')
169 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
168 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
170 assert response.text == content
169 assert response.text == content
171
170
172 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
171 def test_download_file_scoped_to_repo(self, user_util, create_artifact_factory):
173 user = self.log_user()
172 user = self.log_user()
174 user_id = user['user_id']
173 user_id = user['user_id']
175 content = 'HELLO MY NAME IS ARTIFACT !'
174 content = 'HELLO MY NAME IS ARTIFACT !'
176
175
177 artifact = create_artifact_factory(user_id, content)
176 artifact = create_artifact_factory(user_id, content)
178 # bind to repo
177 # bind to repo
179 repo = user_util.create_repo()
178 repo = user_util.create_repo()
180 repo_id = repo.repo_id
179 repo_id = repo.repo_id
181 artifact.scope_repo_id = repo_id
180 artifact.scope_repo_id = repo_id
182 Session().add(artifact)
181 Session().add(artifact)
183 Session().commit()
182 Session().commit()
184
183
185 file_uid = artifact.file_uid
184 file_uid = artifact.file_uid
186 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
185 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
187 assert response.text == content
186 assert response.text == content
188
187
189 # log-in to new user and test download again
188 # log-in to new user and test download again
190 user = user_util.create_user(password='qweqwe')
189 user = user_util.create_user(password='qweqwe')
191 self.log_user(user.username, 'qweqwe')
190 self.log_user(user.username, 'qweqwe')
192 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
191 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
193 assert response.text == content
192 assert response.text == content
194
193
195 # forbid user the rights to repo
194 # forbid user the rights to repo
196 repo = Repository.get(repo_id)
195 repo = Repository.get(repo_id)
197 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
196 user_util.grant_user_permission_to_repo(repo, user, 'repository.none')
198 self.app.get(route_path('download_file', fid=file_uid), status=404)
197 self.app.get(route_path('download_file', fid=file_uid), status=404)
199
198
200 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
199 def test_download_file_scoped_to_user(self, user_util, create_artifact_factory):
201 user = self.log_user()
200 user = self.log_user()
202 user_id = user['user_id']
201 user_id = user['user_id']
203 content = 'HELLO MY NAME IS ARTIFACT !'
202 content = 'HELLO MY NAME IS ARTIFACT !'
204
203
205 artifact = create_artifact_factory(user_id, content)
204 artifact = create_artifact_factory(user_id, content)
206 # bind to user
205 # bind to user
207 user = user_util.create_user(password='qweqwe')
206 user = user_util.create_user(password='qweqwe')
208
207
209 artifact.scope_user_id = user.user_id
208 artifact.scope_user_id = user.user_id
210 Session().add(artifact)
209 Session().add(artifact)
211 Session().commit()
210 Session().commit()
212
211
213 # artifact creator doesn't have access since it's bind to another user
212 # artifact creator doesn't have access since it's bind to another user
214 file_uid = artifact.file_uid
213 file_uid = artifact.file_uid
215 self.app.get(route_path('download_file', fid=file_uid), status=404)
214 self.app.get(route_path('download_file', fid=file_uid), status=404)
216
215
217 # log-in to new user and test download again, should be ok since we're bind to this artifact
216 # log-in to new user and test download again, should be ok since we're bind to this artifact
218 self.log_user(user.username, 'qweqwe')
217 self.log_user(user.username, 'qweqwe')
219 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
218 response = self.app.get(route_path('download_file', fid=file_uid), status=200)
220 assert response.text == content
219 assert response.text == content
221
220
222 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
221 def test_download_file_scoped_to_repo_with_bad_token(self, user_util, create_artifact_factory):
223 user_id = User.get_first_super_admin().user_id
222 user_id = User.get_first_super_admin().user_id
224 content = 'HELLO MY NAME IS ARTIFACT !'
223 content = 'HELLO MY NAME IS ARTIFACT !'
225
224
226 artifact = create_artifact_factory(user_id, content)
225 artifact = create_artifact_factory(user_id, content)
227 # bind to repo
226 # bind to repo
228 repo = user_util.create_repo()
227 repo = user_util.create_repo()
229 repo_id = repo.repo_id
228 repo_id = repo.repo_id
230 artifact.scope_repo_id = repo_id
229 artifact.scope_repo_id = repo_id
231 Session().add(artifact)
230 Session().add(artifact)
232 Session().commit()
231 Session().commit()
233
232
234 file_uid = artifact.file_uid
233 file_uid = artifact.file_uid
235 self.app.get(route_path('download_file_by_token',
234 self.app.get(route_path('download_file_by_token',
236 _auth_token='bogus', fid=file_uid), status=302)
235 _auth_token='bogus', fid=file_uid), status=302)
237
236
238 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
237 def test_download_file_scoped_to_repo_with_token(self, user_util, create_artifact_factory):
239 user = User.get_first_super_admin()
238 user = User.get_first_super_admin()
240 AuthTokenModel().create(user, 'test artifact token',
239 AuthTokenModel().create(user, 'test artifact token',
241 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
240 role=AuthTokenModel.cls.ROLE_ARTIFACT_DOWNLOAD)
242
241
243 user = User.get_first_super_admin()
242 user = User.get_first_super_admin()
244 artifact_token = user.artifact_token
243 artifact_token = user.artifact_token
245
244
246 user_id = User.get_first_super_admin().user_id
245 user_id = User.get_first_super_admin().user_id
247 content = 'HELLO MY NAME IS ARTIFACT !'
246 content = 'HELLO MY NAME IS ARTIFACT !'
248
247
249 artifact = create_artifact_factory(user_id, content)
248 artifact = create_artifact_factory(user_id, content)
250 # bind to repo
249 # bind to repo
251 repo = user_util.create_repo()
250 repo = user_util.create_repo()
252 repo_id = repo.repo_id
251 repo_id = repo.repo_id
253 artifact.scope_repo_id = repo_id
252 artifact.scope_repo_id = repo_id
254 Session().add(artifact)
253 Session().add(artifact)
255 Session().commit()
254 Session().commit()
256
255
257 file_uid = artifact.file_uid
256 file_uid = artifact.file_uid
258 response = self.app.get(
257 response = self.app.get(
259 route_path('download_file_by_token',
258 route_path('download_file_by_token',
260 _auth_token=artifact_token, fid=file_uid), status=200)
259 _auth_token=artifact_token, fid=file_uid), status=200)
261 assert response.text == content
260 assert response.text == content
@@ -1,57 +1,57 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import io
21 import io
22 import uuid
22 import uuid
23 import pathlib
23 import pathlib
24
24
25
25
26 def get_file_storage(settings):
26 def get_file_storage(settings):
27 from rhodecode.apps.file_store.backends.local_store import LocalFileStorage
27 from rhodecode.apps.file_store.backends.local_store import LocalFileStorage
28 from rhodecode.apps.file_store import config_keys
28 from rhodecode.apps.file_store import config_keys
29 store_path = settings.get(config_keys.store_path)
29 store_path = settings.get(config_keys.store_path)
30 return LocalFileStorage(base_path=store_path)
30 return LocalFileStorage(base_path=store_path)
31
31
32
32
33 def splitext(filename):
33 def splitext(filename):
34 ext = ''.join(pathlib.Path(filename).suffixes)
34 ext = ''.join(pathlib.Path(filename).suffixes)
35 return filename, ext
35 return filename, ext
36
36
37
37
38 def uid_filename(filename, randomized=True):
38 def uid_filename(filename, randomized=True):
39 """
39 """
40 Generates a randomized or stable (uuid) filename,
40 Generates a randomized or stable (uuid) filename,
41 preserving the original extension.
41 preserving the original extension.
42
42
43 :param filename: the original filename
43 :param filename: the original filename
44 :param randomized: define if filename should be stable (sha1 based) or randomized
44 :param randomized: define if filename should be stable (sha1 based) or randomized
45 """
45 """
46
46
47 _, ext = splitext(filename)
47 _, ext = splitext(filename)
48 if randomized:
48 if randomized:
49 uid = uuid.uuid4()
49 uid = uuid.uuid4()
50 else:
50 else:
51 hash_key = '{}.{}'.format(filename, 'store')
51 hash_key = '{}.{}'.format(filename, 'store')
52 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
52 uid = uuid.uuid5(uuid.NAMESPACE_URL, hash_key)
53 return str(uid) + ext.lower()
53 return str(uid) + ext.lower()
54
54
55
55
56 def bytes_to_file_obj(bytes_data):
56 def bytes_to_file_obj(bytes_data):
57 return io.StringIO(bytes_data)
57 return io.StringIO(bytes_data)
@@ -1,202 +1,202 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import logging
20 import logging
21
21
22
22
23 from pyramid.response import FileResponse
23 from pyramid.response import FileResponse
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25
25
26 from rhodecode.apps._base import BaseAppView
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps.file_store import utils
27 from rhodecode.apps.file_store import utils
28 from rhodecode.apps.file_store.exceptions import (
28 from rhodecode.apps.file_store.exceptions import (
29 FileNotAllowedException, FileOverSizeException)
29 FileNotAllowedException, FileOverSizeException)
30
30
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import audit_logger
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 LoginRequired)
35 LoginRequired)
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 from rhodecode.model.db import Session, FileStore, UserApiKeys
37 from rhodecode.model.db import Session, FileStore, UserApiKeys
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class FileStoreView(BaseAppView):
42 class FileStoreView(BaseAppView):
43 upload_key = 'store_file'
43 upload_key = 'store_file'
44
44
45 def load_default_context(self):
45 def load_default_context(self):
46 c = self._get_local_tmpl_context()
46 c = self._get_local_tmpl_context()
47 self.storage = utils.get_file_storage(self.request.registry.settings)
47 self.storage = utils.get_file_storage(self.request.registry.settings)
48 return c
48 return c
49
49
50 def _guess_type(self, file_name):
50 def _guess_type(self, file_name):
51 """
51 """
52 Our own type guesser for mimetypes using the rich DB
52 Our own type guesser for mimetypes using the rich DB
53 """
53 """
54 if not hasattr(self, 'db'):
54 if not hasattr(self, 'db'):
55 self.db = get_mimetypes_db()
55 self.db = get_mimetypes_db()
56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
57 return _content_type, _encoding
57 return _content_type, _encoding
58
58
59 def _serve_file(self, file_uid):
59 def _serve_file(self, file_uid):
60 if not self.storage.exists(file_uid):
60 if not self.storage.exists(file_uid):
61 store_path = self.storage.store_path(file_uid)
61 store_path = self.storage.store_path(file_uid)
62 log.debug('File with FID:%s not found in the store under `%s`',
62 log.debug('File with FID:%s not found in the store under `%s`',
63 file_uid, store_path)
63 file_uid, store_path)
64 raise HTTPNotFound()
64 raise HTTPNotFound()
65
65
66 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
66 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
67 if not db_obj:
67 if not db_obj:
68 raise HTTPNotFound()
68 raise HTTPNotFound()
69
69
70 # private upload for user
70 # private upload for user
71 if db_obj.check_acl and db_obj.scope_user_id:
71 if db_obj.check_acl and db_obj.scope_user_id:
72 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
72 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
73 db_obj.scope_user_id)
73 db_obj.scope_user_id)
74 user = db_obj.user
74 user = db_obj.user
75 if self._rhodecode_db_user.user_id != user.user_id:
75 if self._rhodecode_db_user.user_id != user.user_id:
76 log.warning('Access to file store object forbidden')
76 log.warning('Access to file store object forbidden')
77 raise HTTPNotFound()
77 raise HTTPNotFound()
78
78
79 # scoped to repository permissions
79 # scoped to repository permissions
80 if db_obj.check_acl and db_obj.scope_repo_id:
80 if db_obj.check_acl and db_obj.scope_repo_id:
81 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
81 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
82 db_obj.scope_repo_id)
82 db_obj.scope_repo_id)
83 repo = db_obj.repo
83 repo = db_obj.repo
84 perm_set = ['repository.read', 'repository.write', 'repository.admin']
84 perm_set = ['repository.read', 'repository.write', 'repository.admin']
85 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
85 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
86 if not has_perm:
86 if not has_perm:
87 log.warning('Access to file store object `%s` forbidden', file_uid)
87 log.warning('Access to file store object `%s` forbidden', file_uid)
88 raise HTTPNotFound()
88 raise HTTPNotFound()
89
89
90 # scoped to repository group permissions
90 # scoped to repository group permissions
91 if db_obj.check_acl and db_obj.scope_repo_group_id:
91 if db_obj.check_acl and db_obj.scope_repo_group_id:
92 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
92 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
93 db_obj.scope_repo_group_id)
93 db_obj.scope_repo_group_id)
94 repo_group = db_obj.repo_group
94 repo_group = db_obj.repo_group
95 perm_set = ['group.read', 'group.write', 'group.admin']
95 perm_set = ['group.read', 'group.write', 'group.admin']
96 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
96 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
97 if not has_perm:
97 if not has_perm:
98 log.warning('Access to file store object `%s` forbidden', file_uid)
98 log.warning('Access to file store object `%s` forbidden', file_uid)
99 raise HTTPNotFound()
99 raise HTTPNotFound()
100
100
101 FileStore.bump_access_counter(file_uid)
101 FileStore.bump_access_counter(file_uid)
102
102
103 file_path = self.storage.store_path(file_uid)
103 file_path = self.storage.store_path(file_uid)
104 content_type = 'application/octet-stream'
104 content_type = 'application/octet-stream'
105 content_encoding = None
105 content_encoding = None
106
106
107 _content_type, _encoding = self._guess_type(file_path)
107 _content_type, _encoding = self._guess_type(file_path)
108 if _content_type:
108 if _content_type:
109 content_type = _content_type
109 content_type = _content_type
110
110
111 # For file store we don't submit any session data, this logic tells the
111 # For file store we don't submit any session data, this logic tells the
112 # Session lib to skip it
112 # Session lib to skip it
113 setattr(self.request, '_file_response', True)
113 setattr(self.request, '_file_response', True)
114 response = FileResponse(
114 response = FileResponse(
115 file_path, request=self.request,
115 file_path, request=self.request,
116 content_type=content_type, content_encoding=content_encoding)
116 content_type=content_type, content_encoding=content_encoding)
117
117
118 file_name = db_obj.file_display_name
118 file_name = db_obj.file_display_name
119
119
120 response.headers["Content-Disposition"] = (
120 response.headers["Content-Disposition"] = (
121 'attachment; filename="{}"'.format(str(file_name))
121 'attachment; filename="{}"'.format(str(file_name))
122 )
122 )
123 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
123 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
124 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
124 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
125 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
125 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
126 return response
126 return response
127
127
128 @LoginRequired()
128 @LoginRequired()
129 @NotAnonymous()
129 @NotAnonymous()
130 @CSRFRequired()
130 @CSRFRequired()
131 def upload_file(self):
131 def upload_file(self):
132 self.load_default_context()
132 self.load_default_context()
133 file_obj = self.request.POST.get(self.upload_key)
133 file_obj = self.request.POST.get(self.upload_key)
134
134
135 if file_obj is None:
135 if file_obj is None:
136 return {'store_fid': None,
136 return {'store_fid': None,
137 'access_path': None,
137 'access_path': None,
138 'error': '{} data field is missing'.format(self.upload_key)}
138 'error': '{} data field is missing'.format(self.upload_key)}
139
139
140 if not hasattr(file_obj, 'filename'):
140 if not hasattr(file_obj, 'filename'):
141 return {'store_fid': None,
141 return {'store_fid': None,
142 'access_path': None,
142 'access_path': None,
143 'error': 'filename cannot be read from the data field'}
143 'error': 'filename cannot be read from the data field'}
144
144
145 filename = file_obj.filename
145 filename = file_obj.filename
146
146
147 metadata = {
147 metadata = {
148 'user_uploaded': {'username': self._rhodecode_user.username,
148 'user_uploaded': {'username': self._rhodecode_user.username,
149 'user_id': self._rhodecode_user.user_id,
149 'user_id': self._rhodecode_user.user_id,
150 'ip': self._rhodecode_user.ip_addr}}
150 'ip': self._rhodecode_user.ip_addr}}
151 try:
151 try:
152 store_uid, metadata = self.storage.save_file(
152 store_uid, metadata = self.storage.save_file(
153 file_obj.file, filename, extra_metadata=metadata)
153 file_obj.file, filename, extra_metadata=metadata)
154 except FileNotAllowedException:
154 except FileNotAllowedException:
155 return {'store_fid': None,
155 return {'store_fid': None,
156 'access_path': None,
156 'access_path': None,
157 'error': 'File {} is not allowed.'.format(filename)}
157 'error': 'File {} is not allowed.'.format(filename)}
158
158
159 except FileOverSizeException:
159 except FileOverSizeException:
160 return {'store_fid': None,
160 return {'store_fid': None,
161 'access_path': None,
161 'access_path': None,
162 'error': 'File {} is exceeding allowed limit.'.format(filename)}
162 'error': 'File {} is exceeding allowed limit.'.format(filename)}
163
163
164 try:
164 try:
165 entry = FileStore.create(
165 entry = FileStore.create(
166 file_uid=store_uid, filename=metadata["filename"],
166 file_uid=store_uid, filename=metadata["filename"],
167 file_hash=metadata["sha256"], file_size=metadata["size"],
167 file_hash=metadata["sha256"], file_size=metadata["size"],
168 file_description=u'upload attachment',
168 file_description=u'upload attachment',
169 check_acl=False, user_id=self._rhodecode_user.user_id
169 check_acl=False, user_id=self._rhodecode_user.user_id
170 )
170 )
171 Session().add(entry)
171 Session().add(entry)
172 Session().commit()
172 Session().commit()
173 log.debug('Stored upload in DB as %s', entry)
173 log.debug('Stored upload in DB as %s', entry)
174 except Exception:
174 except Exception:
175 log.exception('Failed to store file %s', filename)
175 log.exception('Failed to store file %s', filename)
176 return {'store_fid': None,
176 return {'store_fid': None,
177 'access_path': None,
177 'access_path': None,
178 'error': 'File {} failed to store in DB.'.format(filename)}
178 'error': 'File {} failed to store in DB.'.format(filename)}
179
179
180 return {'store_fid': store_uid,
180 return {'store_fid': store_uid,
181 'access_path': h.route_path('download_file', fid=store_uid)}
181 'access_path': h.route_path('download_file', fid=store_uid)}
182
182
183 # ACL is checked by scopes, if no scope the file is accessible to all
183 # ACL is checked by scopes, if no scope the file is accessible to all
184 def download_file(self):
184 def download_file(self):
185 self.load_default_context()
185 self.load_default_context()
186 file_uid = self.request.matchdict['fid']
186 file_uid = self.request.matchdict['fid']
187 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
187 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
188 return self._serve_file(file_uid)
188 return self._serve_file(file_uid)
189
189
190 # in addition to @LoginRequired ACL is checked by scopes
190 # in addition to @LoginRequired ACL is checked by scopes
191 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
191 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
192 @NotAnonymous()
192 @NotAnonymous()
193 def download_file_by_token(self):
193 def download_file_by_token(self):
194 """
194 """
195 Special view that allows to access the download file by special URL that
195 Special view that allows to access the download file by special URL that
196 is stored inside the URL.
196 is stored inside the URL.
197
197
198 http://example.com/_file_store/token-download/TOKEN/FILE_UID
198 http://example.com/_file_store/token-download/TOKEN/FILE_UID
199 """
199 """
200 self.load_default_context()
200 self.load_default_context()
201 file_uid = self.request.matchdict['fid']
201 file_uid = self.request.matchdict['fid']
202 return self._serve_file(file_uid)
202 return self._serve_file(file_uid)
@@ -1,120 +1,120 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import ADMIN_PREFIX
20 from rhodecode.apps._base import ADMIN_PREFIX
21
21
22
22
23 def admin_routes(config):
23 def admin_routes(config):
24 from rhodecode.apps.gist.views import GistView
24 from rhodecode.apps.gist.views import GistView
25
25
26 config.add_route(
26 config.add_route(
27 name='gists_show', pattern='/gists')
27 name='gists_show', pattern='/gists')
28 config.add_view(
28 config.add_view(
29 GistView,
29 GistView,
30 attr='gist_show_all',
30 attr='gist_show_all',
31 route_name='gists_show', request_method='GET',
31 route_name='gists_show', request_method='GET',
32 renderer='rhodecode:templates/admin/gists/gist_index.mako')
32 renderer='rhodecode:templates/admin/gists/gist_index.mako')
33
33
34 config.add_route(
34 config.add_route(
35 name='gists_new', pattern='/gists/new')
35 name='gists_new', pattern='/gists/new')
36 config.add_view(
36 config.add_view(
37 GistView,
37 GistView,
38 attr='gist_new',
38 attr='gist_new',
39 route_name='gists_new', request_method='GET',
39 route_name='gists_new', request_method='GET',
40 renderer='rhodecode:templates/admin/gists/gist_new.mako')
40 renderer='rhodecode:templates/admin/gists/gist_new.mako')
41
41
42 config.add_route(
42 config.add_route(
43 name='gists_create', pattern='/gists/create')
43 name='gists_create', pattern='/gists/create')
44 config.add_view(
44 config.add_view(
45 GistView,
45 GistView,
46 attr='gist_create',
46 attr='gist_create',
47 route_name='gists_create', request_method='POST',
47 route_name='gists_create', request_method='POST',
48 renderer='rhodecode:templates/admin/gists/gist_new.mako')
48 renderer='rhodecode:templates/admin/gists/gist_new.mako')
49
49
50 config.add_route(
50 config.add_route(
51 name='gist_show', pattern='/gists/{gist_id}')
51 name='gist_show', pattern='/gists/{gist_id}')
52 config.add_view(
52 config.add_view(
53 GistView,
53 GistView,
54 attr='gist_show',
54 attr='gist_show',
55 route_name='gist_show', request_method='GET',
55 route_name='gist_show', request_method='GET',
56 renderer='rhodecode:templates/admin/gists/gist_show.mako')
56 renderer='rhodecode:templates/admin/gists/gist_show.mako')
57
57
58 config.add_route(
58 config.add_route(
59 name='gist_show_rev',
59 name='gist_show_rev',
60 pattern='/gists/{gist_id}/rev/{revision}')
60 pattern='/gists/{gist_id}/rev/{revision}')
61
61
62 config.add_view(
62 config.add_view(
63 GistView,
63 GistView,
64 attr='gist_show',
64 attr='gist_show',
65 route_name='gist_show_rev', request_method='GET',
65 route_name='gist_show_rev', request_method='GET',
66 renderer='rhodecode:templates/admin/gists/gist_show.mako')
66 renderer='rhodecode:templates/admin/gists/gist_show.mako')
67
67
68 config.add_route(
68 config.add_route(
69 name='gist_show_formatted',
69 name='gist_show_formatted',
70 pattern='/gists/{gist_id}/rev/{revision}/{format}')
70 pattern='/gists/{gist_id}/rev/{revision}/{format}')
71 config.add_view(
71 config.add_view(
72 GistView,
72 GistView,
73 attr='gist_show',
73 attr='gist_show',
74 route_name='gist_show_formatted', request_method='GET',
74 route_name='gist_show_formatted', request_method='GET',
75 renderer=None)
75 renderer=None)
76
76
77 config.add_route(
77 config.add_route(
78 name='gist_show_formatted_path',
78 name='gist_show_formatted_path',
79 pattern='/gists/{gist_id}/rev/{revision}/{format}/{f_path:.*}')
79 pattern='/gists/{gist_id}/rev/{revision}/{format}/{f_path:.*}')
80 config.add_view(
80 config.add_view(
81 GistView,
81 GistView,
82 attr='gist_show',
82 attr='gist_show',
83 route_name='gist_show_formatted_path', request_method='GET',
83 route_name='gist_show_formatted_path', request_method='GET',
84 renderer=None)
84 renderer=None)
85
85
86 config.add_route(
86 config.add_route(
87 name='gist_delete', pattern='/gists/{gist_id}/delete')
87 name='gist_delete', pattern='/gists/{gist_id}/delete')
88 config.add_view(
88 config.add_view(
89 GistView,
89 GistView,
90 attr='gist_delete',
90 attr='gist_delete',
91 route_name='gist_delete', request_method='POST')
91 route_name='gist_delete', request_method='POST')
92
92
93 config.add_route(
93 config.add_route(
94 name='gist_edit', pattern='/gists/{gist_id}/edit')
94 name='gist_edit', pattern='/gists/{gist_id}/edit')
95 config.add_view(
95 config.add_view(
96 GistView,
96 GistView,
97 attr='gist_edit',
97 attr='gist_edit',
98 route_name='gist_edit', request_method='GET',
98 route_name='gist_edit', request_method='GET',
99 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
99 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
100
100
101 config.add_route(
101 config.add_route(
102 name='gist_update', pattern='/gists/{gist_id}/update')
102 name='gist_update', pattern='/gists/{gist_id}/update')
103 config.add_view(
103 config.add_view(
104 GistView,
104 GistView,
105 attr='gist_update',
105 attr='gist_update',
106 route_name='gist_update', request_method='POST',
106 route_name='gist_update', request_method='POST',
107 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
107 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
108
108
109 config.add_route(
109 config.add_route(
110 name='gist_edit_check_revision',
110 name='gist_edit_check_revision',
111 pattern='/gists/{gist_id}/edit/check_revision')
111 pattern='/gists/{gist_id}/edit/check_revision')
112 config.add_view(
112 config.add_view(
113 GistView,
113 GistView,
114 attr='gist_edit_check_revision',
114 attr='gist_edit_check_revision',
115 route_name='gist_edit_check_revision', request_method='GET',
115 route_name='gist_edit_check_revision', request_method='GET',
116 renderer='json_ext')
116 renderer='json_ext')
117
117
118
118
119 def includeme(config):
119 def includeme(config):
120 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
120 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,20 +1,20 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
@@ -1,391 +1,390 b''
1 # -*- coding: utf-8 -*-
2
1
3 # Copyright (C) 2010-2020 RhodeCode GmbH
2 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
3 #
5 # This program is free software: you can redistribute it and/or modify
4 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
5 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
6 # (only), as published by the Free Software Foundation.
8 #
7 #
9 # This program is distributed in the hope that it will be useful,
8 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
11 # GNU General Public License for more details.
13 #
12 #
14 # You should have received a copy of the GNU Affero General Public License
13 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
15 #
17 # This program is dual-licensed. If you wish to learn more about the
16 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
19
21 import mock
20 import mock
22 import pytest
21 import pytest
23
22
24 from rhodecode.lib import helpers as h
23 from rhodecode.lib import helpers as h
25 from rhodecode.model.db import User, Gist
24 from rhodecode.model.db import User, Gist
26 from rhodecode.model.gist import GistModel
25 from rhodecode.model.gist import GistModel
27 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
27 from rhodecode.tests import (
29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
28 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 TestController, assert_session_flash)
29 TestController, assert_session_flash)
31
30
32
31
33 def route_path(name, params=None, **kwargs):
32 def route_path(name, params=None, **kwargs):
34 import urllib.request, urllib.parse, urllib.error
33 import urllib.request, urllib.parse, urllib.error
35 from rhodecode.apps._base import ADMIN_PREFIX
34 from rhodecode.apps._base import ADMIN_PREFIX
36
35
37 base_url = {
36 base_url = {
38 'gists_show': ADMIN_PREFIX + '/gists',
37 'gists_show': ADMIN_PREFIX + '/gists',
39 'gists_new': ADMIN_PREFIX + '/gists/new',
38 'gists_new': ADMIN_PREFIX + '/gists/new',
40 'gists_create': ADMIN_PREFIX + '/gists/create',
39 'gists_create': ADMIN_PREFIX + '/gists/create',
41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
40 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
41 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
42 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
43 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
44 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
45 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
46 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
47 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
49
48
50 }[name].format(**kwargs)
49 }[name].format(**kwargs)
51
50
52 if params:
51 if params:
53 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
52 base_url = '{}?{}'.format(base_url, urllib.parse.urlencode(params))
54 return base_url
53 return base_url
55
54
56
55
57 class GistUtility(object):
56 class GistUtility(object):
58
57
59 def __init__(self):
58 def __init__(self):
60 self._gist_ids = []
59 self._gist_ids = []
61
60
62 def __call__(
61 def __call__(
63 self, f_name, content='some gist', lifetime=-1,
62 self, f_name, content='some gist', lifetime=-1,
64 description='gist-desc', gist_type='public',
63 description='gist-desc', gist_type='public',
65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
64 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
66 gist_mapping = {
65 gist_mapping = {
67 f_name: {'content': content}
66 f_name: {'content': content}
68 }
67 }
69 user = User.get_by_username(owner)
68 user = User.get_by_username(owner)
70 gist = GistModel().create(
69 gist = GistModel().create(
71 description, owner=user, gist_mapping=gist_mapping,
70 description, owner=user, gist_mapping=gist_mapping,
72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
71 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
73 Session().commit()
72 Session().commit()
74 self._gist_ids.append(gist.gist_id)
73 self._gist_ids.append(gist.gist_id)
75 return gist
74 return gist
76
75
77 def cleanup(self):
76 def cleanup(self):
78 for gist_id in self._gist_ids:
77 for gist_id in self._gist_ids:
79 gist = Gist.get(gist_id)
78 gist = Gist.get(gist_id)
80 if gist:
79 if gist:
81 Session().delete(gist)
80 Session().delete(gist)
82
81
83 Session().commit()
82 Session().commit()
84
83
85
84
86 @pytest.fixture()
85 @pytest.fixture()
87 def create_gist(request):
86 def create_gist(request):
88 gist_utility = GistUtility()
87 gist_utility = GistUtility()
89 request.addfinalizer(gist_utility.cleanup)
88 request.addfinalizer(gist_utility.cleanup)
90 return gist_utility
89 return gist_utility
91
90
92
91
93 class TestGistsController(TestController):
92 class TestGistsController(TestController):
94
93
95 def test_index_empty(self, create_gist):
94 def test_index_empty(self, create_gist):
96 self.log_user()
95 self.log_user()
97 response = self.app.get(route_path('gists_show'))
96 response = self.app.get(route_path('gists_show'))
98 response.mustcontain('data: [],')
97 response.mustcontain('data: [],')
99
98
100 def test_index(self, create_gist):
99 def test_index(self, create_gist):
101 self.log_user()
100 self.log_user()
102 g1 = create_gist('gist1')
101 g1 = create_gist('gist1')
103 g2 = create_gist('gist2', lifetime=1400)
102 g2 = create_gist('gist2', lifetime=1400)
104 g3 = create_gist('gist3', description='gist3-desc')
103 g3 = create_gist('gist3', description='gist3-desc')
105 g4 = create_gist('gist4', gist_type='private').gist_access_id
104 g4 = create_gist('gist4', gist_type='private').gist_access_id
106 response = self.app.get(route_path('gists_show'))
105 response = self.app.get(route_path('gists_show'))
107
106
108 response.mustcontain(g1.gist_access_id)
107 response.mustcontain(g1.gist_access_id)
109 response.mustcontain(g2.gist_access_id)
108 response.mustcontain(g2.gist_access_id)
110 response.mustcontain(g3.gist_access_id)
109 response.mustcontain(g3.gist_access_id)
111 response.mustcontain('gist3-desc')
110 response.mustcontain('gist3-desc')
112 response.mustcontain(no=[g4])
111 response.mustcontain(no=[g4])
113
112
114 # Expiration information should be visible
113 # Expiration information should be visible
115 expires_tag = '%s' % h.age_component(
114 expires_tag = '%s' % h.age_component(
116 h.time_to_utcdatetime(g2.gist_expires))
115 h.time_to_utcdatetime(g2.gist_expires))
117 response.mustcontain(expires_tag.replace('"', '\\"'))
116 response.mustcontain(expires_tag.replace('"', '\\"'))
118
117
119 def test_index_private_gists(self, create_gist):
118 def test_index_private_gists(self, create_gist):
120 self.log_user()
119 self.log_user()
121 gist = create_gist('gist5', gist_type='private')
120 gist = create_gist('gist5', gist_type='private')
122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
121 response = self.app.get(route_path('gists_show', params=dict(private=1)))
123
122
124 # and privates
123 # and privates
125 response.mustcontain(gist.gist_access_id)
124 response.mustcontain(gist.gist_access_id)
126
125
127 def test_index_show_all(self, create_gist):
126 def test_index_show_all(self, create_gist):
128 self.log_user()
127 self.log_user()
129 create_gist('gist1')
128 create_gist('gist1')
130 create_gist('gist2', lifetime=1400)
129 create_gist('gist2', lifetime=1400)
131 create_gist('gist3', description='gist3-desc')
130 create_gist('gist3', description='gist3-desc')
132 create_gist('gist4', gist_type='private')
131 create_gist('gist4', gist_type='private')
133
132
134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
133 response = self.app.get(route_path('gists_show', params=dict(all=1)))
135
134
136 assert len(GistModel.get_all()) == 4
135 assert len(GistModel.get_all()) == 4
137 # and privates
136 # and privates
138 for gist in GistModel.get_all():
137 for gist in GistModel.get_all():
139 response.mustcontain(gist.gist_access_id)
138 response.mustcontain(gist.gist_access_id)
140
139
141 def test_index_show_all_hidden_from_regular(self, create_gist):
140 def test_index_show_all_hidden_from_regular(self, create_gist):
142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
141 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
143 create_gist('gist2', gist_type='private')
142 create_gist('gist2', gist_type='private')
144 create_gist('gist3', gist_type='private')
143 create_gist('gist3', gist_type='private')
145 create_gist('gist4', gist_type='private')
144 create_gist('gist4', gist_type='private')
146
145
147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
146 response = self.app.get(route_path('gists_show', params=dict(all=1)))
148
147
149 assert len(GistModel.get_all()) == 3
148 assert len(GistModel.get_all()) == 3
150 # since we don't have access to private in this view, we
149 # since we don't have access to private in this view, we
151 # should see nothing
150 # should see nothing
152 for gist in GistModel.get_all():
151 for gist in GistModel.get_all():
153 response.mustcontain(no=[gist.gist_access_id])
152 response.mustcontain(no=[gist.gist_access_id])
154
153
155 def test_create(self):
154 def test_create(self):
156 self.log_user()
155 self.log_user()
157 response = self.app.post(
156 response = self.app.post(
158 route_path('gists_create'),
157 route_path('gists_create'),
159 params={'lifetime': -1,
158 params={'lifetime': -1,
160 'content': 'gist test',
159 'content': 'gist test',
161 'filename': 'foo',
160 'filename': 'foo',
162 'gist_type': 'public',
161 'gist_type': 'public',
163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
162 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 'csrf_token': self.csrf_token},
163 'csrf_token': self.csrf_token},
165 status=302)
164 status=302)
166 response = response.follow()
165 response = response.follow()
167 response.mustcontain('added file: foo')
166 response.mustcontain('added file: foo')
168 response.mustcontain('gist test')
167 response.mustcontain('gist test')
169
168
170 def test_create_with_path_with_dirs(self):
169 def test_create_with_path_with_dirs(self):
171 self.log_user()
170 self.log_user()
172 response = self.app.post(
171 response = self.app.post(
173 route_path('gists_create'),
172 route_path('gists_create'),
174 params={'lifetime': -1,
173 params={'lifetime': -1,
175 'content': 'gist test',
174 'content': 'gist test',
176 'filename': '/home/foo',
175 'filename': '/home/foo',
177 'gist_type': 'public',
176 'gist_type': 'public',
178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
177 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
179 'csrf_token': self.csrf_token},
178 'csrf_token': self.csrf_token},
180 status=200)
179 status=200)
181 response.mustcontain('Filename /home/foo cannot be inside a directory')
180 response.mustcontain('Filename /home/foo cannot be inside a directory')
182
181
183 def test_access_expired_gist(self, create_gist):
182 def test_access_expired_gist(self, create_gist):
184 self.log_user()
183 self.log_user()
185 gist = create_gist('never-see-me')
184 gist = create_gist('never-see-me')
186 gist.gist_expires = 0 # 1970
185 gist.gist_expires = 0 # 1970
187 Session().add(gist)
186 Session().add(gist)
188 Session().commit()
187 Session().commit()
189
188
190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
189 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
191 status=404)
190 status=404)
192
191
193 def test_create_private(self):
192 def test_create_private(self):
194 self.log_user()
193 self.log_user()
195 response = self.app.post(
194 response = self.app.post(
196 route_path('gists_create'),
195 route_path('gists_create'),
197 params={'lifetime': -1,
196 params={'lifetime': -1,
198 'content': 'private gist test',
197 'content': 'private gist test',
199 'filename': 'private-foo',
198 'filename': 'private-foo',
200 'gist_type': 'private',
199 'gist_type': 'private',
201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
200 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
202 'csrf_token': self.csrf_token},
201 'csrf_token': self.csrf_token},
203 status=302)
202 status=302)
204 response = response.follow()
203 response = response.follow()
205 response.mustcontain('added file: private-foo<')
204 response.mustcontain('added file: private-foo<')
206 response.mustcontain('private gist test')
205 response.mustcontain('private gist test')
207 response.mustcontain('Private Gist')
206 response.mustcontain('Private Gist')
208 # Make sure private gists are not indexed by robots
207 # Make sure private gists are not indexed by robots
209 response.mustcontain(
208 response.mustcontain(
210 '<meta name="robots" content="noindex, nofollow">')
209 '<meta name="robots" content="noindex, nofollow">')
211
210
212 def test_create_private_acl_private(self):
211 def test_create_private_acl_private(self):
213 self.log_user()
212 self.log_user()
214 response = self.app.post(
213 response = self.app.post(
215 route_path('gists_create'),
214 route_path('gists_create'),
216 params={'lifetime': -1,
215 params={'lifetime': -1,
217 'content': 'private gist test',
216 'content': 'private gist test',
218 'filename': 'private-foo',
217 'filename': 'private-foo',
219 'gist_type': 'private',
218 'gist_type': 'private',
220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
219 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
221 'csrf_token': self.csrf_token},
220 'csrf_token': self.csrf_token},
222 status=302)
221 status=302)
223 response = response.follow()
222 response = response.follow()
224 response.mustcontain('added file: private-foo<')
223 response.mustcontain('added file: private-foo<')
225 response.mustcontain('private gist test')
224 response.mustcontain('private gist test')
226 response.mustcontain('Private Gist')
225 response.mustcontain('Private Gist')
227 # Make sure private gists are not indexed by robots
226 # Make sure private gists are not indexed by robots
228 response.mustcontain(
227 response.mustcontain(
229 '<meta name="robots" content="noindex, nofollow">')
228 '<meta name="robots" content="noindex, nofollow">')
230
229
231 def test_create_with_description(self):
230 def test_create_with_description(self):
232 self.log_user()
231 self.log_user()
233 response = self.app.post(
232 response = self.app.post(
234 route_path('gists_create'),
233 route_path('gists_create'),
235 params={'lifetime': -1,
234 params={'lifetime': -1,
236 'content': 'gist test',
235 'content': 'gist test',
237 'filename': 'foo-desc',
236 'filename': 'foo-desc',
238 'description': 'gist-desc',
237 'description': 'gist-desc',
239 'gist_type': 'public',
238 'gist_type': 'public',
240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
239 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
241 'csrf_token': self.csrf_token},
240 'csrf_token': self.csrf_token},
242 status=302)
241 status=302)
243 response = response.follow()
242 response = response.follow()
244 response.mustcontain('added file: foo-desc')
243 response.mustcontain('added file: foo-desc')
245 response.mustcontain('gist test')
244 response.mustcontain('gist test')
246 response.mustcontain('gist-desc')
245 response.mustcontain('gist-desc')
247
246
248 def test_create_public_with_anonymous_access(self):
247 def test_create_public_with_anonymous_access(self):
249 self.log_user()
248 self.log_user()
250 params = {
249 params = {
251 'lifetime': -1,
250 'lifetime': -1,
252 'content': 'gist test',
251 'content': 'gist test',
253 'filename': 'foo-desc',
252 'filename': 'foo-desc',
254 'description': 'gist-desc',
253 'description': 'gist-desc',
255 'gist_type': 'public',
254 'gist_type': 'public',
256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
255 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
257 'csrf_token': self.csrf_token
256 'csrf_token': self.csrf_token
258 }
257 }
259 response = self.app.post(
258 response = self.app.post(
260 route_path('gists_create'), params=params, status=302)
259 route_path('gists_create'), params=params, status=302)
261 self.logout_user()
260 self.logout_user()
262 response = response.follow()
261 response = response.follow()
263 response.mustcontain('added file: foo-desc')
262 response.mustcontain('added file: foo-desc')
264 response.mustcontain('gist test')
263 response.mustcontain('gist test')
265 response.mustcontain('gist-desc')
264 response.mustcontain('gist-desc')
266
265
267 def test_new(self):
266 def test_new(self):
268 self.log_user()
267 self.log_user()
269 self.app.get(route_path('gists_new'))
268 self.app.get(route_path('gists_new'))
270
269
271 def test_delete(self, create_gist):
270 def test_delete(self, create_gist):
272 self.log_user()
271 self.log_user()
273 gist = create_gist('delete-me')
272 gist = create_gist('delete-me')
274 response = self.app.post(
273 response = self.app.post(
275 route_path('gist_delete', gist_id=gist.gist_id),
274 route_path('gist_delete', gist_id=gist.gist_id),
276 params={'csrf_token': self.csrf_token})
275 params={'csrf_token': self.csrf_token})
277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
276 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
278
277
279 def test_delete_normal_user_his_gist(self, create_gist):
278 def test_delete_normal_user_his_gist(self, create_gist):
280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
279 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
280 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
282
281
283 response = self.app.post(
282 response = self.app.post(
284 route_path('gist_delete', gist_id=gist.gist_id),
283 route_path('gist_delete', gist_id=gist.gist_id),
285 params={'csrf_token': self.csrf_token})
284 params={'csrf_token': self.csrf_token})
286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
285 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
287
286
288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
287 def test_delete_normal_user_not_his_own_gist(self, create_gist):
289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
288 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
290 gist = create_gist('delete-me-2')
289 gist = create_gist('delete-me-2')
291
290
292 self.app.post(
291 self.app.post(
293 route_path('gist_delete', gist_id=gist.gist_id),
292 route_path('gist_delete', gist_id=gist.gist_id),
294 params={'csrf_token': self.csrf_token}, status=404)
293 params={'csrf_token': self.csrf_token}, status=404)
295
294
296 def test_show(self, create_gist):
295 def test_show(self, create_gist):
297 gist = create_gist('gist-show-me')
296 gist = create_gist('gist-show-me')
298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
297 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
299
298
300 response.mustcontain('added file: gist-show-me<')
299 response.mustcontain('added file: gist-show-me<')
301
300
302 assert_response = response.assert_response()
301 assert_response = response.assert_response()
303 assert_response.element_equals_to(
302 assert_response.element_equals_to(
304 'div.rc-user span.user',
303 'div.rc-user span.user',
305 '<a href="/_profiles/test_admin">test_admin</a>')
304 '<a href="/_profiles/test_admin">test_admin</a>')
306
305
307 response.mustcontain('gist-desc')
306 response.mustcontain('gist-desc')
308
307
309 def test_show_without_hg(self, create_gist):
308 def test_show_without_hg(self, create_gist):
310 with mock.patch(
309 with mock.patch(
311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
310 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
312 gist = create_gist('gist-show-me-again')
311 gist = create_gist('gist-show-me-again')
313 self.app.get(
312 self.app.get(
314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
313 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
315
314
316 def test_show_acl_private(self, create_gist):
315 def test_show_acl_private(self, create_gist):
317 gist = create_gist('gist-show-me-only-when-im-logged-in',
316 gist = create_gist('gist-show-me-only-when-im-logged-in',
318 acl_level=Gist.ACL_LEVEL_PRIVATE)
317 acl_level=Gist.ACL_LEVEL_PRIVATE)
319 self.app.get(
318 self.app.get(
320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
319 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
321
320
322 # now we log-in we should see thi gist
321 # now we log-in we should see thi gist
323 self.log_user()
322 self.log_user()
324 response = self.app.get(
323 response = self.app.get(
325 route_path('gist_show', gist_id=gist.gist_access_id))
324 route_path('gist_show', gist_id=gist.gist_access_id))
326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
325 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
327
326
328 assert_response = response.assert_response()
327 assert_response = response.assert_response()
329 assert_response.element_equals_to(
328 assert_response.element_equals_to(
330 'div.rc-user span.user',
329 'div.rc-user span.user',
331 '<a href="/_profiles/test_admin">test_admin</a>')
330 '<a href="/_profiles/test_admin">test_admin</a>')
332 response.mustcontain('gist-desc')
331 response.mustcontain('gist-desc')
333
332
334 def test_show_as_raw(self, create_gist):
333 def test_show_as_raw(self, create_gist):
335 gist = create_gist('gist-show-me', content='GIST CONTENT')
334 gist = create_gist('gist-show-me', content='GIST CONTENT')
336 response = self.app.get(
335 response = self.app.get(
337 route_path('gist_show_formatted',
336 route_path('gist_show_formatted',
338 gist_id=gist.gist_access_id, revision='tip',
337 gist_id=gist.gist_access_id, revision='tip',
339 format='raw'))
338 format='raw'))
340 assert response.text == 'GIST CONTENT'
339 assert response.text == 'GIST CONTENT'
341
340
342 def test_show_as_raw_individual_file(self, create_gist):
341 def test_show_as_raw_individual_file(self, create_gist):
343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
342 gist = create_gist('gist-show-me-raw', content='GIST BODY')
344 response = self.app.get(
343 response = self.app.get(
345 route_path('gist_show_formatted_path',
344 route_path('gist_show_formatted_path',
346 gist_id=gist.gist_access_id, format='raw',
345 gist_id=gist.gist_access_id, format='raw',
347 revision='tip', f_path='gist-show-me-raw'))
346 revision='tip', f_path='gist-show-me-raw'))
348 assert response.text == 'GIST BODY'
347 assert response.text == 'GIST BODY'
349
348
350 def test_edit_page(self, create_gist):
349 def test_edit_page(self, create_gist):
351 self.log_user()
350 self.log_user()
352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
351 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
352 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
354 response.mustcontain('GIST EDIT BODY')
353 response.mustcontain('GIST EDIT BODY')
355
354
356 def test_edit_page_non_logged_user(self, create_gist):
355 def test_edit_page_non_logged_user(self, create_gist):
357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
356 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
357 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
359 status=302)
358 status=302)
360
359
361 def test_edit_normal_user_his_gist(self, create_gist):
360 def test_edit_normal_user_his_gist(self, create_gist):
362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
361 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
362 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
363 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
365 status=200))
364 status=200))
366
365
367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
366 def test_edit_normal_user_not_his_own_gist(self, create_gist):
368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
367 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
369 gist = create_gist('delete-me')
368 gist = create_gist('delete-me')
370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
369 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
371 status=404)
370 status=404)
372
371
373 def test_user_first_name_is_escaped(self, user_util, create_gist):
372 def test_user_first_name_is_escaped(self, user_util, create_gist):
374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
373 xss_atack_string = '"><script>alert(\'First Name\')</script>'
375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
374 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
376 password = 'test'
375 password = 'test'
377 user = user_util.create_user(
376 user = user_util.create_user(
378 firstname=xss_atack_string, password=password)
377 firstname=xss_atack_string, password=password)
379 create_gist('gist', gist_type='public', owner=user.username)
378 create_gist('gist', gist_type='public', owner=user.username)
380 response = self.app.get(route_path('gists_show'))
379 response = self.app.get(route_path('gists_show'))
381 response.mustcontain(xss_escaped_string)
380 response.mustcontain(xss_escaped_string)
382
381
383 def test_user_last_name_is_escaped(self, user_util, create_gist):
382 def test_user_last_name_is_escaped(self, user_util, create_gist):
384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
383 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
384 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
386 password = 'test'
385 password = 'test'
387 user = user_util.create_user(
386 user = user_util.create_user(
388 lastname=xss_atack_string, password=password)
387 lastname=xss_atack_string, password=password)
389 create_gist('gist', gist_type='public', owner=user.username)
388 create_gist('gist', gist_type='public', owner=user.username)
390 response = self.app.get(route_path('gists_show'))
389 response = self.app.get(route_path('gists_show'))
391 response.mustcontain(xss_escaped_string)
390 response.mustcontain(xss_escaped_string)
@@ -1,378 +1,378 b''
1 # -*- coding: utf-8 -*-
1
2
2
3 # Copyright (C) 2013-2020 RhodeCode GmbH
3 # Copyright (C) 2013-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import time
21 import time
22 import logging
22 import logging
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode.apps._base import BaseAppView
32 from rhodecode.apps._base import BaseAppView
33 from rhodecode.lib import helpers as h, ext_json
33 from rhodecode.lib import helpers as h, ext_json
34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.utils2 import time_to_datetime
35 from rhodecode.lib.utils2 import time_to_datetime
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 from rhodecode.model.gist import GistModel
38 from rhodecode.model.gist import GistModel
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 from rhodecode.model.db import Gist, User, or_
40 from rhodecode.model.db import Gist, User, or_
41 from rhodecode.model import validation_schema
41 from rhodecode.model import validation_schema
42 from rhodecode.model.validation_schema.schemas import gist_schema
42 from rhodecode.model.validation_schema.schemas import gist_schema
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class GistView(BaseAppView):
48 class GistView(BaseAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 _ = self.request.translate
51 _ = self.request.translate
52 c = self._get_local_tmpl_context()
52 c = self._get_local_tmpl_context()
53 c.user = c.auth_user.get_instance()
53 c.user = c.auth_user.get_instance()
54
54
55 c.lifetime_values = [
55 c.lifetime_values = [
56 (-1, _('forever')),
56 (-1, _('forever')),
57 (5, _('5 minutes')),
57 (5, _('5 minutes')),
58 (60, _('1 hour')),
58 (60, _('1 hour')),
59 (60 * 24, _('1 day')),
59 (60 * 24, _('1 day')),
60 (60 * 24 * 30, _('1 month')),
60 (60 * 24 * 30, _('1 month')),
61 ]
61 ]
62
62
63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 c.acl_options = [
64 c.acl_options = [
65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 ]
67 ]
68
68
69 return c
69 return c
70
70
71 @LoginRequired()
71 @LoginRequired()
72 def gist_show_all(self):
72 def gist_show_all(self):
73 c = self.load_default_context()
73 c = self.load_default_context()
74
74
75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
76 c.show_private = self.request.GET.get('private') and not_default_user
76 c.show_private = self.request.GET.get('private') and not_default_user
77 c.show_public = self.request.GET.get('public') and not_default_user
77 c.show_public = self.request.GET.get('public') and not_default_user
78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
79
79
80 gists = _gists = Gist().query()\
80 gists = _gists = Gist().query()\
81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
82 .order_by(Gist.created_on.desc())
82 .order_by(Gist.created_on.desc())
83
83
84 c.active = 'public'
84 c.active = 'public'
85 # MY private
85 # MY private
86 if c.show_private and not c.show_public:
86 if c.show_private and not c.show_public:
87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
89 c.active = 'my_private'
89 c.active = 'my_private'
90 # MY public
90 # MY public
91 elif c.show_public and not c.show_private:
91 elif c.show_public and not c.show_private:
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 c.active = 'my_public'
94 c.active = 'my_public'
95 # MY public+private
95 # MY public+private
96 elif c.show_private and c.show_public:
96 elif c.show_private and c.show_public:
97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
98 Gist.gist_type == Gist.GIST_PRIVATE))\
98 Gist.gist_type == Gist.GIST_PRIVATE))\
99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
100 c.active = 'my_all'
100 c.active = 'my_all'
101 # Show all by super-admin
101 # Show all by super-admin
102 elif c.show_all:
102 elif c.show_all:
103 c.active = 'all'
103 c.active = 'all'
104 gists = _gists
104 gists = _gists
105
105
106 # default show ALL public gists
106 # default show ALL public gists
107 if not c.show_public and not c.show_private and not c.show_all:
107 if not c.show_public and not c.show_private and not c.show_all:
108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
109 c.active = 'public'
109 c.active = 'public'
110
110
111 _render = self.request.get_partial_renderer(
111 _render = self.request.get_partial_renderer(
112 'rhodecode:templates/data_table/_dt_elements.mako')
112 'rhodecode:templates/data_table/_dt_elements.mako')
113
113
114 data = []
114 data = []
115
115
116 for gist in gists:
116 for gist in gists:
117 data.append({
117 data.append({
118 'created_on': _render('gist_created', gist.created_on),
118 'created_on': _render('gist_created', gist.created_on),
119 'created_on_raw': gist.created_on,
119 'created_on_raw': gist.created_on,
120 'type': _render('gist_type', gist.gist_type),
120 'type': _render('gist_type', gist.gist_type),
121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
123 'author_raw': h.escape(gist.owner.full_contact),
123 'author_raw': h.escape(gist.owner.full_contact),
124 'expires': _render('gist_expires', gist.gist_expires),
124 'expires': _render('gist_expires', gist.gist_expires),
125 'description': _render('gist_description', gist.gist_description)
125 'description': _render('gist_description', gist.gist_description)
126 })
126 })
127 c.data = ext_json.str_json(data)
127 c.data = ext_json.str_json(data)
128
128
129 return self._get_template_context(c)
129 return self._get_template_context(c)
130
130
131 @LoginRequired()
131 @LoginRequired()
132 @NotAnonymous()
132 @NotAnonymous()
133 def gist_new(self):
133 def gist_new(self):
134 c = self.load_default_context()
134 c = self.load_default_context()
135 return self._get_template_context(c)
135 return self._get_template_context(c)
136
136
137 @LoginRequired()
137 @LoginRequired()
138 @NotAnonymous()
138 @NotAnonymous()
139 @CSRFRequired()
139 @CSRFRequired()
140 def gist_create(self):
140 def gist_create(self):
141 _ = self.request.translate
141 _ = self.request.translate
142 c = self.load_default_context()
142 c = self.load_default_context()
143
143
144 data = dict(self.request.POST)
144 data = dict(self.request.POST)
145
145
146 filename = data.pop('filename', '') or Gist.DEFAULT_FILENAME
146 filename = data.pop('filename', '') or Gist.DEFAULT_FILENAME
147
147
148 data['nodes'] = [{
148 data['nodes'] = [{
149 'filename': filename,
149 'filename': filename,
150 'content': data.pop('content', ''),
150 'content': data.pop('content', ''),
151 'mimetype': data.pop('mimetype', None) # None is autodetect
151 'mimetype': data.pop('mimetype', None) # None is autodetect
152 }]
152 }]
153
153
154 schema = gist_schema.GistSchema().bind(
154 schema = gist_schema.GistSchema().bind(
155 lifetime_options=[x[0] for x in c.lifetime_values])
155 lifetime_options=[x[0] for x in c.lifetime_values])
156
156
157 try:
157 try:
158
158
159 schema_data = schema.deserialize(data)
159 schema_data = schema.deserialize(data)
160
160
161 # convert to safer format with just KEYs so we sure no duplicates
161 # convert to safer format with just KEYs so we sure no duplicates
162 schema_data['nodes'] = gist_schema.sequence_to_nodes(schema_data['nodes'])
162 schema_data['nodes'] = gist_schema.sequence_to_nodes(schema_data['nodes'])
163
163
164 gist = GistModel().create(
164 gist = GistModel().create(
165 gist_id=schema_data['gistid'], # custom access id not real ID
165 gist_id=schema_data['gistid'], # custom access id not real ID
166 description=schema_data['description'],
166 description=schema_data['description'],
167 owner=self._rhodecode_user.user_id,
167 owner=self._rhodecode_user.user_id,
168 gist_mapping=schema_data['nodes'],
168 gist_mapping=schema_data['nodes'],
169 gist_type=schema_data['gist_type'],
169 gist_type=schema_data['gist_type'],
170 lifetime=schema_data['lifetime'],
170 lifetime=schema_data['lifetime'],
171 gist_acl_level=schema_data['gist_acl_level']
171 gist_acl_level=schema_data['gist_acl_level']
172 )
172 )
173 Session().commit()
173 Session().commit()
174 new_gist_id = gist.gist_access_id
174 new_gist_id = gist.gist_access_id
175 except validation_schema.Invalid as errors:
175 except validation_schema.Invalid as errors:
176 defaults = data
176 defaults = data
177 errors = errors.asdict()
177 errors = errors.asdict()
178
178
179 if 'nodes.0.content' in errors:
179 if 'nodes.0.content' in errors:
180 errors['content'] = errors['nodes.0.content']
180 errors['content'] = errors['nodes.0.content']
181 del errors['nodes.0.content']
181 del errors['nodes.0.content']
182 if 'nodes.0.filename' in errors:
182 if 'nodes.0.filename' in errors:
183 errors['filename'] = errors['nodes.0.filename']
183 errors['filename'] = errors['nodes.0.filename']
184 del errors['nodes.0.filename']
184 del errors['nodes.0.filename']
185
185
186 data = render('rhodecode:templates/admin/gists/gist_new.mako',
186 data = render('rhodecode:templates/admin/gists/gist_new.mako',
187 self._get_template_context(c), self.request)
187 self._get_template_context(c), self.request)
188 html = formencode.htmlfill.render(
188 html = formencode.htmlfill.render(
189 data,
189 data,
190 defaults=defaults,
190 defaults=defaults,
191 errors=errors,
191 errors=errors,
192 prefix_error=False,
192 prefix_error=False,
193 encoding="UTF-8",
193 encoding="UTF-8",
194 force_defaults=False
194 force_defaults=False
195 )
195 )
196 return Response(html)
196 return Response(html)
197
197
198 except Exception:
198 except Exception:
199 log.exception("Exception while trying to create a gist")
199 log.exception("Exception while trying to create a gist")
200 h.flash(_('Error occurred during gist creation'), category='error')
200 h.flash(_('Error occurred during gist creation'), category='error')
201 raise HTTPFound(h.route_url('gists_new'))
201 raise HTTPFound(h.route_url('gists_new'))
202 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
202 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
203
203
204 @LoginRequired()
204 @LoginRequired()
205 @NotAnonymous()
205 @NotAnonymous()
206 @CSRFRequired()
206 @CSRFRequired()
207 def gist_delete(self):
207 def gist_delete(self):
208 _ = self.request.translate
208 _ = self.request.translate
209 gist_id = self.request.matchdict['gist_id']
209 gist_id = self.request.matchdict['gist_id']
210
210
211 c = self.load_default_context()
211 c = self.load_default_context()
212 c.gist = Gist.get_or_404(gist_id)
212 c.gist = Gist.get_or_404(gist_id)
213
213
214 owner = c.gist.gist_owner == self._rhodecode_user.user_id
214 owner = c.gist.gist_owner == self._rhodecode_user.user_id
215 if not (h.HasPermissionAny('hg.admin')() or owner):
215 if not (h.HasPermissionAny('hg.admin')() or owner):
216 log.warning('Deletion of Gist was forbidden '
216 log.warning('Deletion of Gist was forbidden '
217 'by unauthorized user: `%s`', self._rhodecode_user)
217 'by unauthorized user: `%s`', self._rhodecode_user)
218 raise HTTPNotFound()
218 raise HTTPNotFound()
219
219
220 GistModel().delete(c.gist)
220 GistModel().delete(c.gist)
221 Session().commit()
221 Session().commit()
222 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
222 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
223
223
224 raise HTTPFound(h.route_url('gists_show'))
224 raise HTTPFound(h.route_url('gists_show'))
225
225
226 def _get_gist(self, gist_id):
226 def _get_gist(self, gist_id):
227
227
228 gist = Gist.get_or_404(gist_id)
228 gist = Gist.get_or_404(gist_id)
229
229
230 # Check if this gist is expired
230 # Check if this gist is expired
231 if gist.gist_expires != -1:
231 if gist.gist_expires != -1:
232 if time.time() > gist.gist_expires:
232 if time.time() > gist.gist_expires:
233 log.error(
233 log.error(
234 'Gist expired at %s', time_to_datetime(gist.gist_expires))
234 'Gist expired at %s', time_to_datetime(gist.gist_expires))
235 raise HTTPNotFound()
235 raise HTTPNotFound()
236
236
237 # check if this gist requires a login
237 # check if this gist requires a login
238 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
238 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
239 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
239 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
240 log.error("Anonymous user %s tried to access protected gist `%s`",
240 log.error("Anonymous user %s tried to access protected gist `%s`",
241 self._rhodecode_user, gist_id)
241 self._rhodecode_user, gist_id)
242 raise HTTPNotFound()
242 raise HTTPNotFound()
243 return gist
243 return gist
244
244
245 @LoginRequired()
245 @LoginRequired()
246 def gist_show(self):
246 def gist_show(self):
247 gist_id = self.request.matchdict['gist_id']
247 gist_id = self.request.matchdict['gist_id']
248
248
249 # TODO(marcink): expose those via matching dict
249 # TODO(marcink): expose those via matching dict
250 revision = self.request.matchdict.get('revision', 'tip')
250 revision = self.request.matchdict.get('revision', 'tip')
251 f_path = self.request.matchdict.get('f_path', None)
251 f_path = self.request.matchdict.get('f_path', None)
252 return_format = self.request.matchdict.get('format')
252 return_format = self.request.matchdict.get('format')
253
253
254 c = self.load_default_context()
254 c = self.load_default_context()
255 c.gist = self._get_gist(gist_id)
255 c.gist = self._get_gist(gist_id)
256 c.render = not self.request.GET.get('no-render', False)
256 c.render = not self.request.GET.get('no-render', False)
257
257
258 try:
258 try:
259 c.file_last_commit, c.files = GistModel().get_gist_files(
259 c.file_last_commit, c.files = GistModel().get_gist_files(
260 gist_id, revision=revision)
260 gist_id, revision=revision)
261 except VCSError:
261 except VCSError:
262 log.exception("Exception in gist show")
262 log.exception("Exception in gist show")
263 raise HTTPNotFound()
263 raise HTTPNotFound()
264
264
265 if return_format == 'raw':
265 if return_format == 'raw':
266 content = '\n\n'.join([f.content for f in c.files
266 content = '\n\n'.join([f.content for f in c.files
267 if (f_path is None or f.path == f_path)])
267 if (f_path is None or f.path == f_path)])
268 response = Response(content)
268 response = Response(content)
269 response.content_type = 'text/plain'
269 response.content_type = 'text/plain'
270 return response
270 return response
271 elif return_format:
271 elif return_format:
272 raise HTTPBadRequest()
272 raise HTTPBadRequest()
273
273
274 return self._get_template_context(c)
274 return self._get_template_context(c)
275
275
276 @LoginRequired()
276 @LoginRequired()
277 @NotAnonymous()
277 @NotAnonymous()
278 def gist_edit(self):
278 def gist_edit(self):
279 _ = self.request.translate
279 _ = self.request.translate
280 gist_id = self.request.matchdict['gist_id']
280 gist_id = self.request.matchdict['gist_id']
281 c = self.load_default_context()
281 c = self.load_default_context()
282 c.gist = self._get_gist(gist_id)
282 c.gist = self._get_gist(gist_id)
283
283
284 owner = c.gist.gist_owner == self._rhodecode_user.user_id
284 owner = c.gist.gist_owner == self._rhodecode_user.user_id
285 if not (h.HasPermissionAny('hg.admin')() or owner):
285 if not (h.HasPermissionAny('hg.admin')() or owner):
286 raise HTTPNotFound()
286 raise HTTPNotFound()
287
287
288 try:
288 try:
289 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
289 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
290 except VCSError:
290 except VCSError:
291 log.exception("Exception in gist edit")
291 log.exception("Exception in gist edit")
292 raise HTTPNotFound()
292 raise HTTPNotFound()
293
293
294 if c.gist.gist_expires == -1:
294 if c.gist.gist_expires == -1:
295 expiry = _('never')
295 expiry = _('never')
296 else:
296 else:
297 # this cannot use timeago, since it's used in select2 as a value
297 # this cannot use timeago, since it's used in select2 as a value
298 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
298 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
299
299
300 c.lifetime_values.append(
300 c.lifetime_values.append(
301 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
301 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
302 )
302 )
303
303
304 return self._get_template_context(c)
304 return self._get_template_context(c)
305
305
306 @LoginRequired()
306 @LoginRequired()
307 @NotAnonymous()
307 @NotAnonymous()
308 @CSRFRequired()
308 @CSRFRequired()
309 def gist_update(self):
309 def gist_update(self):
310 _ = self.request.translate
310 _ = self.request.translate
311 gist_id = self.request.matchdict['gist_id']
311 gist_id = self.request.matchdict['gist_id']
312 c = self.load_default_context()
312 c = self.load_default_context()
313 c.gist = self._get_gist(gist_id)
313 c.gist = self._get_gist(gist_id)
314
314
315 owner = c.gist.gist_owner == self._rhodecode_user.user_id
315 owner = c.gist.gist_owner == self._rhodecode_user.user_id
316 if not (h.HasPermissionAny('hg.admin')() or owner):
316 if not (h.HasPermissionAny('hg.admin')() or owner):
317 raise HTTPNotFound()
317 raise HTTPNotFound()
318
318
319 data = peppercorn.parse(self.request.POST.items())
319 data = peppercorn.parse(self.request.POST.items())
320
320
321 schema = gist_schema.GistSchema()
321 schema = gist_schema.GistSchema()
322 schema = schema.bind(
322 schema = schema.bind(
323 # '0' is special value to leave lifetime untouched
323 # '0' is special value to leave lifetime untouched
324 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
324 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
325 )
325 )
326
326
327 try:
327 try:
328 schema_data = schema.deserialize(data)
328 schema_data = schema.deserialize(data)
329 # convert to safer format with just KEYs so we sure no duplicates
329 # convert to safer format with just KEYs so we sure no duplicates
330 schema_data['nodes'] = gist_schema.sequence_to_nodes(
330 schema_data['nodes'] = gist_schema.sequence_to_nodes(
331 schema_data['nodes'])
331 schema_data['nodes'])
332
332
333 GistModel().update(
333 GistModel().update(
334 gist=c.gist,
334 gist=c.gist,
335 description=schema_data['description'],
335 description=schema_data['description'],
336 owner=c.gist.owner,
336 owner=c.gist.owner,
337 gist_mapping=schema_data['nodes'],
337 gist_mapping=schema_data['nodes'],
338 lifetime=schema_data['lifetime'],
338 lifetime=schema_data['lifetime'],
339 gist_acl_level=schema_data['gist_acl_level']
339 gist_acl_level=schema_data['gist_acl_level']
340 )
340 )
341
341
342 Session().commit()
342 Session().commit()
343 h.flash(_('Successfully updated gist content'), category='success')
343 h.flash(_('Successfully updated gist content'), category='success')
344 except NodeNotChangedError:
344 except NodeNotChangedError:
345 # raised if nothing was changed in repo itself. We anyway then
345 # raised if nothing was changed in repo itself. We anyway then
346 # store only DB stuff for gist
346 # store only DB stuff for gist
347 Session().commit()
347 Session().commit()
348 h.flash(_('Successfully updated gist data'), category='success')
348 h.flash(_('Successfully updated gist data'), category='success')
349 except validation_schema.Invalid as errors:
349 except validation_schema.Invalid as errors:
350 errors = h.escape(errors.asdict())
350 errors = h.escape(errors.asdict())
351 h.flash(_('Error occurred during update of gist {}: {}').format(
351 h.flash(_('Error occurred during update of gist {}: {}').format(
352 gist_id, errors), category='error')
352 gist_id, errors), category='error')
353 except Exception:
353 except Exception:
354 log.exception("Exception in gist edit")
354 log.exception("Exception in gist edit")
355 h.flash(_('Error occurred during update of gist %s') % gist_id,
355 h.flash(_('Error occurred during update of gist %s') % gist_id,
356 category='error')
356 category='error')
357
357
358 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
358 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @NotAnonymous()
361 @NotAnonymous()
362 def gist_edit_check_revision(self):
362 def gist_edit_check_revision(self):
363 _ = self.request.translate
363 _ = self.request.translate
364 gist_id = self.request.matchdict['gist_id']
364 gist_id = self.request.matchdict['gist_id']
365 c = self.load_default_context()
365 c = self.load_default_context()
366 c.gist = self._get_gist(gist_id)
366 c.gist = self._get_gist(gist_id)
367
367
368 last_rev = c.gist.scm_instance().get_commit()
368 last_rev = c.gist.scm_instance().get_commit()
369 success = True
369 success = True
370 revision = self.request.GET.get('revision')
370 revision = self.request.GET.get('revision')
371
371
372 if revision != last_rev.raw_id:
372 if revision != last_rev.raw_id:
373 log.error('Last revision %s is different then submitted %s',
373 log.error('Last revision %s is different then submitted %s',
374 revision, last_rev)
374 revision, last_rev)
375 # our gist has newer version than we
375 # our gist has newer version than we
376 success = False
376 success = False
377
377
378 return {'success': success}
378 return {'success': success}
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now