##// END OF EJS Templates
caches: use new decorator that uses conditional caches skipping dogpile if cache is disabled.
marcink -
r2892:c633266d default
parent child Browse files
Show More
@@ -1,1297 +1,1297 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 itertools
21 import itertools
22 import logging
22 import logging
23 import os
23 import os
24 import shutil
24 import shutil
25 import tempfile
25 import tempfile
26 import collections
26 import collections
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.apps._base import RepoAppView
34 from rhodecode.apps._base import RepoAppView
35
35
36 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, caches, rc_cache
37 from rhodecode.lib import diffs, helpers as h, caches, rc_cache
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.exceptions import NonRelativePathError
39 from rhodecode.lib.exceptions import NonRelativePathError
40 from rhodecode.lib.codeblocks import (
40 from rhodecode.lib.codeblocks import (
41 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
42 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
43 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
43 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
46 from rhodecode.lib.vcs import path as vcspath
46 from rhodecode.lib.vcs import path as vcspath
47 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.conf import settings
48 from rhodecode.lib.vcs.conf import settings
49 from rhodecode.lib.vcs.nodes import FileNode
49 from rhodecode.lib.vcs.nodes import FileNode
50 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 NodeDoesNotExistError, CommitError, NodeError)
53 NodeDoesNotExistError, CommitError, NodeError)
54
54
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.db import Repository
56 from rhodecode.model.db import Repository
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 class RepoFilesView(RepoAppView):
61 class RepoFilesView(RepoAppView):
62
62
63 @staticmethod
63 @staticmethod
64 def adjust_file_path_for_svn(f_path, repo):
64 def adjust_file_path_for_svn(f_path, repo):
65 """
65 """
66 Computes the relative path of `f_path`.
66 Computes the relative path of `f_path`.
67
67
68 This is mainly based on prefix matching of the recognized tags and
68 This is mainly based on prefix matching of the recognized tags and
69 branches in the underlying repository.
69 branches in the underlying repository.
70 """
70 """
71 tags_and_branches = itertools.chain(
71 tags_and_branches = itertools.chain(
72 repo.branches.iterkeys(),
72 repo.branches.iterkeys(),
73 repo.tags.iterkeys())
73 repo.tags.iterkeys())
74 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
75
75
76 for name in tags_and_branches:
76 for name in tags_and_branches:
77 if f_path.startswith('{}/'.format(name)):
77 if f_path.startswith('{}/'.format(name)):
78 f_path = vcspath.relpath(f_path, name)
78 f_path = vcspath.relpath(f_path, name)
79 break
79 break
80 return f_path
80 return f_path
81
81
82 def load_default_context(self):
82 def load_default_context(self):
83 c = self._get_local_tmpl_context(include_app_defaults=True)
83 c = self._get_local_tmpl_context(include_app_defaults=True)
84 c.rhodecode_repo = self.rhodecode_vcs_repo
84 c.rhodecode_repo = self.rhodecode_vcs_repo
85 return c
85 return c
86
86
87 def _ensure_not_locked(self):
87 def _ensure_not_locked(self):
88 _ = self.request.translate
88 _ = self.request.translate
89
89
90 repo = self.db_repo
90 repo = self.db_repo
91 if repo.enable_locking and repo.locked[0]:
91 if repo.enable_locking and repo.locked[0]:
92 h.flash(_('This repository has been locked by %s on %s')
92 h.flash(_('This repository has been locked by %s on %s')
93 % (h.person_by_id(repo.locked[0]),
93 % (h.person_by_id(repo.locked[0]),
94 h.format_date(h.time_to_datetime(repo.locked[1]))),
94 h.format_date(h.time_to_datetime(repo.locked[1]))),
95 'warning')
95 'warning')
96 files_url = h.route_path(
96 files_url = h.route_path(
97 'repo_files:default_path',
97 'repo_files:default_path',
98 repo_name=self.db_repo_name, commit_id='tip')
98 repo_name=self.db_repo_name, commit_id='tip')
99 raise HTTPFound(files_url)
99 raise HTTPFound(files_url)
100
100
101 def _get_commit_and_path(self):
101 def _get_commit_and_path(self):
102 default_commit_id = self.db_repo.landing_rev[1]
102 default_commit_id = self.db_repo.landing_rev[1]
103 default_f_path = '/'
103 default_f_path = '/'
104
104
105 commit_id = self.request.matchdict.get(
105 commit_id = self.request.matchdict.get(
106 'commit_id', default_commit_id)
106 'commit_id', default_commit_id)
107 f_path = self._get_f_path(self.request.matchdict, default_f_path)
107 f_path = self._get_f_path(self.request.matchdict, default_f_path)
108 return commit_id, f_path
108 return commit_id, f_path
109
109
110 def _get_default_encoding(self, c):
110 def _get_default_encoding(self, c):
111 enc_list = getattr(c, 'default_encodings', [])
111 enc_list = getattr(c, 'default_encodings', [])
112 return enc_list[0] if enc_list else 'UTF-8'
112 return enc_list[0] if enc_list else 'UTF-8'
113
113
114 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
114 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
115 """
115 """
116 This is a safe way to get commit. If an error occurs it redirects to
116 This is a safe way to get commit. If an error occurs it redirects to
117 tip with proper message
117 tip with proper message
118
118
119 :param commit_id: id of commit to fetch
119 :param commit_id: id of commit to fetch
120 :param redirect_after: toggle redirection
120 :param redirect_after: toggle redirection
121 """
121 """
122 _ = self.request.translate
122 _ = self.request.translate
123
123
124 try:
124 try:
125 return self.rhodecode_vcs_repo.get_commit(commit_id)
125 return self.rhodecode_vcs_repo.get_commit(commit_id)
126 except EmptyRepositoryError:
126 except EmptyRepositoryError:
127 if not redirect_after:
127 if not redirect_after:
128 return None
128 return None
129
129
130 _url = h.route_path(
130 _url = h.route_path(
131 'repo_files_add_file',
131 'repo_files_add_file',
132 repo_name=self.db_repo_name, commit_id=0, f_path='',
132 repo_name=self.db_repo_name, commit_id=0, f_path='',
133 _anchor='edit')
133 _anchor='edit')
134
134
135 if h.HasRepoPermissionAny(
135 if h.HasRepoPermissionAny(
136 'repository.write', 'repository.admin')(self.db_repo_name):
136 'repository.write', 'repository.admin')(self.db_repo_name):
137 add_new = h.link_to(
137 add_new = h.link_to(
138 _('Click here to add a new file.'), _url, class_="alert-link")
138 _('Click here to add a new file.'), _url, class_="alert-link")
139 else:
139 else:
140 add_new = ""
140 add_new = ""
141
141
142 h.flash(h.literal(
142 h.flash(h.literal(
143 _('There are no files yet. %s') % add_new), category='warning')
143 _('There are no files yet. %s') % add_new), category='warning')
144 raise HTTPFound(
144 raise HTTPFound(
145 h.route_path('repo_summary', repo_name=self.db_repo_name))
145 h.route_path('repo_summary', repo_name=self.db_repo_name))
146
146
147 except (CommitDoesNotExistError, LookupError):
147 except (CommitDoesNotExistError, LookupError):
148 msg = _('No such commit exists for this repository')
148 msg = _('No such commit exists for this repository')
149 h.flash(msg, category='error')
149 h.flash(msg, category='error')
150 raise HTTPNotFound()
150 raise HTTPNotFound()
151 except RepositoryError as e:
151 except RepositoryError as e:
152 h.flash(safe_str(h.escape(e)), category='error')
152 h.flash(safe_str(h.escape(e)), category='error')
153 raise HTTPNotFound()
153 raise HTTPNotFound()
154
154
155 def _get_filenode_or_redirect(self, commit_obj, path):
155 def _get_filenode_or_redirect(self, commit_obj, path):
156 """
156 """
157 Returns file_node, if error occurs or given path is directory,
157 Returns file_node, if error occurs or given path is directory,
158 it'll redirect to top level path
158 it'll redirect to top level path
159 """
159 """
160 _ = self.request.translate
160 _ = self.request.translate
161
161
162 try:
162 try:
163 file_node = commit_obj.get_node(path)
163 file_node = commit_obj.get_node(path)
164 if file_node.is_dir():
164 if file_node.is_dir():
165 raise RepositoryError('The given path is a directory')
165 raise RepositoryError('The given path is a directory')
166 except CommitDoesNotExistError:
166 except CommitDoesNotExistError:
167 log.exception('No such commit exists for this repository')
167 log.exception('No such commit exists for this repository')
168 h.flash(_('No such commit exists for this repository'), category='error')
168 h.flash(_('No such commit exists for this repository'), category='error')
169 raise HTTPNotFound()
169 raise HTTPNotFound()
170 except RepositoryError as e:
170 except RepositoryError as e:
171 log.warning('Repository error while fetching '
171 log.warning('Repository error while fetching '
172 'filenode `%s`. Err:%s', path, e)
172 'filenode `%s`. Err:%s', path, e)
173 h.flash(safe_str(h.escape(e)), category='error')
173 h.flash(safe_str(h.escape(e)), category='error')
174 raise HTTPNotFound()
174 raise HTTPNotFound()
175
175
176 return file_node
176 return file_node
177
177
178 def _is_valid_head(self, commit_id, repo):
178 def _is_valid_head(self, commit_id, repo):
179 # check if commit is a branch identifier- basically we cannot
179 # check if commit is a branch identifier- basically we cannot
180 # create multiple heads via file editing
180 # create multiple heads via file editing
181 valid_heads = repo.branches.keys() + repo.branches.values()
181 valid_heads = repo.branches.keys() + repo.branches.values()
182
182
183 if h.is_svn(repo) and not repo.is_empty():
183 if h.is_svn(repo) and not repo.is_empty():
184 # Note: Subversion only has one head, we add it here in case there
184 # Note: Subversion only has one head, we add it here in case there
185 # is no branch matched.
185 # is no branch matched.
186 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
186 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
187
187
188 # check if commit is a branch name or branch hash
188 # check if commit is a branch name or branch hash
189 return commit_id in valid_heads
189 return commit_id in valid_heads
190
190
191 def _get_tree_at_commit(
191 def _get_tree_at_commit(
192 self, c, commit_id, f_path, full_load=False):
192 self, c, commit_id, f_path, full_load=False):
193
193
194 repo_id = self.db_repo.repo_id
194 repo_id = self.db_repo.repo_id
195
195
196 cache_seconds = safe_int(
196 cache_seconds = safe_int(
197 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
197 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
198 cache_on = cache_seconds > 0
198 cache_on = cache_seconds > 0
199 log.debug(
199 log.debug(
200 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
200 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
201 'with caching: %s[TTL: %ss]' % (
201 'with caching: %s[TTL: %ss]' % (
202 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
202 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
203
203
204 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
204 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
205 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
205 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
206
206
207 @region.cache_on_arguments(namespace=cache_namespace_uid,
207 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
208 should_cache_fn=lambda v: cache_on)
208 condition=cache_on)
209 def compute_file_tree(repo_id, commit_id, f_path, full_load):
209 def compute_file_tree(repo_id, commit_id, f_path, full_load):
210 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
210 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
211 repo_id, commit_id, f_path)
211 repo_id, commit_id, f_path)
212
212
213 c.full_load = full_load
213 c.full_load = full_load
214 return render(
214 return render(
215 'rhodecode:templates/files/files_browser_tree.mako',
215 'rhodecode:templates/files/files_browser_tree.mako',
216 self._get_template_context(c), self.request)
216 self._get_template_context(c), self.request)
217
217
218 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
218 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
219
219
220 def _get_archive_spec(self, fname):
220 def _get_archive_spec(self, fname):
221 log.debug('Detecting archive spec for: `%s`', fname)
221 log.debug('Detecting archive spec for: `%s`', fname)
222
222
223 fileformat = None
223 fileformat = None
224 ext = None
224 ext = None
225 content_type = None
225 content_type = None
226 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
226 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
227 content_type, extension = ext_data
227 content_type, extension = ext_data
228
228
229 if fname.endswith(extension):
229 if fname.endswith(extension):
230 fileformat = a_type
230 fileformat = a_type
231 log.debug('archive is of type: %s', fileformat)
231 log.debug('archive is of type: %s', fileformat)
232 ext = extension
232 ext = extension
233 break
233 break
234
234
235 if not fileformat:
235 if not fileformat:
236 raise ValueError()
236 raise ValueError()
237
237
238 # left over part of whole fname is the commit
238 # left over part of whole fname is the commit
239 commit_id = fname[:-len(ext)]
239 commit_id = fname[:-len(ext)]
240
240
241 return commit_id, ext, fileformat, content_type
241 return commit_id, ext, fileformat, content_type
242
242
243 @LoginRequired()
243 @LoginRequired()
244 @HasRepoPermissionAnyDecorator(
244 @HasRepoPermissionAnyDecorator(
245 'repository.read', 'repository.write', 'repository.admin')
245 'repository.read', 'repository.write', 'repository.admin')
246 @view_config(
246 @view_config(
247 route_name='repo_archivefile', request_method='GET',
247 route_name='repo_archivefile', request_method='GET',
248 renderer=None)
248 renderer=None)
249 def repo_archivefile(self):
249 def repo_archivefile(self):
250 # archive cache config
250 # archive cache config
251 from rhodecode import CONFIG
251 from rhodecode import CONFIG
252 _ = self.request.translate
252 _ = self.request.translate
253 self.load_default_context()
253 self.load_default_context()
254
254
255 fname = self.request.matchdict['fname']
255 fname = self.request.matchdict['fname']
256 subrepos = self.request.GET.get('subrepos') == 'true'
256 subrepos = self.request.GET.get('subrepos') == 'true'
257
257
258 if not self.db_repo.enable_downloads:
258 if not self.db_repo.enable_downloads:
259 return Response(_('Downloads disabled'))
259 return Response(_('Downloads disabled'))
260
260
261 try:
261 try:
262 commit_id, ext, fileformat, content_type = \
262 commit_id, ext, fileformat, content_type = \
263 self._get_archive_spec(fname)
263 self._get_archive_spec(fname)
264 except ValueError:
264 except ValueError:
265 return Response(_('Unknown archive type for: `{}`').format(
265 return Response(_('Unknown archive type for: `{}`').format(
266 h.escape(fname)))
266 h.escape(fname)))
267
267
268 try:
268 try:
269 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
269 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
270 except CommitDoesNotExistError:
270 except CommitDoesNotExistError:
271 return Response(_('Unknown commit_id {}').format(
271 return Response(_('Unknown commit_id {}').format(
272 h.escape(commit_id)))
272 h.escape(commit_id)))
273 except EmptyRepositoryError:
273 except EmptyRepositoryError:
274 return Response(_('Empty repository'))
274 return Response(_('Empty repository'))
275
275
276 archive_name = '%s-%s%s%s' % (
276 archive_name = '%s-%s%s%s' % (
277 safe_str(self.db_repo_name.replace('/', '_')),
277 safe_str(self.db_repo_name.replace('/', '_')),
278 '-sub' if subrepos else '',
278 '-sub' if subrepos else '',
279 safe_str(commit.short_id), ext)
279 safe_str(commit.short_id), ext)
280
280
281 use_cached_archive = False
281 use_cached_archive = False
282 archive_cache_enabled = CONFIG.get(
282 archive_cache_enabled = CONFIG.get(
283 'archive_cache_dir') and not self.request.GET.get('no_cache')
283 'archive_cache_dir') and not self.request.GET.get('no_cache')
284
284
285 if archive_cache_enabled:
285 if archive_cache_enabled:
286 # check if we it's ok to write
286 # check if we it's ok to write
287 if not os.path.isdir(CONFIG['archive_cache_dir']):
287 if not os.path.isdir(CONFIG['archive_cache_dir']):
288 os.makedirs(CONFIG['archive_cache_dir'])
288 os.makedirs(CONFIG['archive_cache_dir'])
289 cached_archive_path = os.path.join(
289 cached_archive_path = os.path.join(
290 CONFIG['archive_cache_dir'], archive_name)
290 CONFIG['archive_cache_dir'], archive_name)
291 if os.path.isfile(cached_archive_path):
291 if os.path.isfile(cached_archive_path):
292 log.debug('Found cached archive in %s', cached_archive_path)
292 log.debug('Found cached archive in %s', cached_archive_path)
293 fd, archive = None, cached_archive_path
293 fd, archive = None, cached_archive_path
294 use_cached_archive = True
294 use_cached_archive = True
295 else:
295 else:
296 log.debug('Archive %s is not yet cached', archive_name)
296 log.debug('Archive %s is not yet cached', archive_name)
297
297
298 if not use_cached_archive:
298 if not use_cached_archive:
299 # generate new archive
299 # generate new archive
300 fd, archive = tempfile.mkstemp()
300 fd, archive = tempfile.mkstemp()
301 log.debug('Creating new temp archive in %s', archive)
301 log.debug('Creating new temp archive in %s', archive)
302 try:
302 try:
303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
303 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
304 except ImproperArchiveTypeError:
304 except ImproperArchiveTypeError:
305 return _('Unknown archive type')
305 return _('Unknown archive type')
306 if archive_cache_enabled:
306 if archive_cache_enabled:
307 # if we generated the archive and we have cache enabled
307 # if we generated the archive and we have cache enabled
308 # let's use this for future
308 # let's use this for future
309 log.debug('Storing new archive in %s', cached_archive_path)
309 log.debug('Storing new archive in %s', cached_archive_path)
310 shutil.move(archive, cached_archive_path)
310 shutil.move(archive, cached_archive_path)
311 archive = cached_archive_path
311 archive = cached_archive_path
312
312
313 # store download action
313 # store download action
314 audit_logger.store_web(
314 audit_logger.store_web(
315 'repo.archive.download', action_data={
315 'repo.archive.download', action_data={
316 'user_agent': self.request.user_agent,
316 'user_agent': self.request.user_agent,
317 'archive_name': archive_name,
317 'archive_name': archive_name,
318 'archive_spec': fname,
318 'archive_spec': fname,
319 'archive_cached': use_cached_archive},
319 'archive_cached': use_cached_archive},
320 user=self._rhodecode_user,
320 user=self._rhodecode_user,
321 repo=self.db_repo,
321 repo=self.db_repo,
322 commit=True
322 commit=True
323 )
323 )
324
324
325 def get_chunked_archive(archive):
325 def get_chunked_archive(archive):
326 with open(archive, 'rb') as stream:
326 with open(archive, 'rb') as stream:
327 while True:
327 while True:
328 data = stream.read(16 * 1024)
328 data = stream.read(16 * 1024)
329 if not data:
329 if not data:
330 if fd: # fd means we used temporary file
330 if fd: # fd means we used temporary file
331 os.close(fd)
331 os.close(fd)
332 if not archive_cache_enabled:
332 if not archive_cache_enabled:
333 log.debug('Destroying temp archive %s', archive)
333 log.debug('Destroying temp archive %s', archive)
334 os.remove(archive)
334 os.remove(archive)
335 break
335 break
336 yield data
336 yield data
337
337
338 response = Response(app_iter=get_chunked_archive(archive))
338 response = Response(app_iter=get_chunked_archive(archive))
339 response.content_disposition = str(
339 response.content_disposition = str(
340 'attachment; filename=%s' % archive_name)
340 'attachment; filename=%s' % archive_name)
341 response.content_type = str(content_type)
341 response.content_type = str(content_type)
342
342
343 return response
343 return response
344
344
345 def _get_file_node(self, commit_id, f_path):
345 def _get_file_node(self, commit_id, f_path):
346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
346 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
347 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
348 try:
348 try:
349 node = commit.get_node(f_path)
349 node = commit.get_node(f_path)
350 if node.is_dir():
350 if node.is_dir():
351 raise NodeError('%s path is a %s not a file'
351 raise NodeError('%s path is a %s not a file'
352 % (node, type(node)))
352 % (node, type(node)))
353 except NodeDoesNotExistError:
353 except NodeDoesNotExistError:
354 commit = EmptyCommit(
354 commit = EmptyCommit(
355 commit_id=commit_id,
355 commit_id=commit_id,
356 idx=commit.idx,
356 idx=commit.idx,
357 repo=commit.repository,
357 repo=commit.repository,
358 alias=commit.repository.alias,
358 alias=commit.repository.alias,
359 message=commit.message,
359 message=commit.message,
360 author=commit.author,
360 author=commit.author,
361 date=commit.date)
361 date=commit.date)
362 node = FileNode(f_path, '', commit=commit)
362 node = FileNode(f_path, '', commit=commit)
363 else:
363 else:
364 commit = EmptyCommit(
364 commit = EmptyCommit(
365 repo=self.rhodecode_vcs_repo,
365 repo=self.rhodecode_vcs_repo,
366 alias=self.rhodecode_vcs_repo.alias)
366 alias=self.rhodecode_vcs_repo.alias)
367 node = FileNode(f_path, '', commit=commit)
367 node = FileNode(f_path, '', commit=commit)
368 return node
368 return node
369
369
370 @LoginRequired()
370 @LoginRequired()
371 @HasRepoPermissionAnyDecorator(
371 @HasRepoPermissionAnyDecorator(
372 'repository.read', 'repository.write', 'repository.admin')
372 'repository.read', 'repository.write', 'repository.admin')
373 @view_config(
373 @view_config(
374 route_name='repo_files_diff', request_method='GET',
374 route_name='repo_files_diff', request_method='GET',
375 renderer=None)
375 renderer=None)
376 def repo_files_diff(self):
376 def repo_files_diff(self):
377 c = self.load_default_context()
377 c = self.load_default_context()
378 f_path = self._get_f_path(self.request.matchdict)
378 f_path = self._get_f_path(self.request.matchdict)
379 diff1 = self.request.GET.get('diff1', '')
379 diff1 = self.request.GET.get('diff1', '')
380 diff2 = self.request.GET.get('diff2', '')
380 diff2 = self.request.GET.get('diff2', '')
381
381
382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
382 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
383
383
384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
384 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
385 line_context = self.request.GET.get('context', 3)
385 line_context = self.request.GET.get('context', 3)
386
386
387 if not any((diff1, diff2)):
387 if not any((diff1, diff2)):
388 h.flash(
388 h.flash(
389 'Need query parameter "diff1" or "diff2" to generate a diff.',
389 'Need query parameter "diff1" or "diff2" to generate a diff.',
390 category='error')
390 category='error')
391 raise HTTPBadRequest()
391 raise HTTPBadRequest()
392
392
393 c.action = self.request.GET.get('diff')
393 c.action = self.request.GET.get('diff')
394 if c.action not in ['download', 'raw']:
394 if c.action not in ['download', 'raw']:
395 compare_url = h.route_path(
395 compare_url = h.route_path(
396 'repo_compare',
396 'repo_compare',
397 repo_name=self.db_repo_name,
397 repo_name=self.db_repo_name,
398 source_ref_type='rev',
398 source_ref_type='rev',
399 source_ref=diff1,
399 source_ref=diff1,
400 target_repo=self.db_repo_name,
400 target_repo=self.db_repo_name,
401 target_ref_type='rev',
401 target_ref_type='rev',
402 target_ref=diff2,
402 target_ref=diff2,
403 _query=dict(f_path=f_path))
403 _query=dict(f_path=f_path))
404 # redirect to new view if we render diff
404 # redirect to new view if we render diff
405 raise HTTPFound(compare_url)
405 raise HTTPFound(compare_url)
406
406
407 try:
407 try:
408 node1 = self._get_file_node(diff1, path1)
408 node1 = self._get_file_node(diff1, path1)
409 node2 = self._get_file_node(diff2, f_path)
409 node2 = self._get_file_node(diff2, f_path)
410 except (RepositoryError, NodeError):
410 except (RepositoryError, NodeError):
411 log.exception("Exception while trying to get node from repository")
411 log.exception("Exception while trying to get node from repository")
412 raise HTTPFound(
412 raise HTTPFound(
413 h.route_path('repo_files', repo_name=self.db_repo_name,
413 h.route_path('repo_files', repo_name=self.db_repo_name,
414 commit_id='tip', f_path=f_path))
414 commit_id='tip', f_path=f_path))
415
415
416 if all(isinstance(node.commit, EmptyCommit)
416 if all(isinstance(node.commit, EmptyCommit)
417 for node in (node1, node2)):
417 for node in (node1, node2)):
418 raise HTTPNotFound()
418 raise HTTPNotFound()
419
419
420 c.commit_1 = node1.commit
420 c.commit_1 = node1.commit
421 c.commit_2 = node2.commit
421 c.commit_2 = node2.commit
422
422
423 if c.action == 'download':
423 if c.action == 'download':
424 _diff = diffs.get_gitdiff(node1, node2,
424 _diff = diffs.get_gitdiff(node1, node2,
425 ignore_whitespace=ignore_whitespace,
425 ignore_whitespace=ignore_whitespace,
426 context=line_context)
426 context=line_context)
427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
428
428
429 response = Response(self.path_filter.get_raw_patch(diff))
429 response = Response(self.path_filter.get_raw_patch(diff))
430 response.content_type = 'text/plain'
430 response.content_type = 'text/plain'
431 response.content_disposition = (
431 response.content_disposition = (
432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
432 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
433 )
433 )
434 charset = self._get_default_encoding(c)
434 charset = self._get_default_encoding(c)
435 if charset:
435 if charset:
436 response.charset = charset
436 response.charset = charset
437 return response
437 return response
438
438
439 elif c.action == 'raw':
439 elif c.action == 'raw':
440 _diff = diffs.get_gitdiff(node1, node2,
440 _diff = diffs.get_gitdiff(node1, node2,
441 ignore_whitespace=ignore_whitespace,
441 ignore_whitespace=ignore_whitespace,
442 context=line_context)
442 context=line_context)
443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
443 diff = diffs.DiffProcessor(_diff, format='gitdiff')
444
444
445 response = Response(self.path_filter.get_raw_patch(diff))
445 response = Response(self.path_filter.get_raw_patch(diff))
446 response.content_type = 'text/plain'
446 response.content_type = 'text/plain'
447 charset = self._get_default_encoding(c)
447 charset = self._get_default_encoding(c)
448 if charset:
448 if charset:
449 response.charset = charset
449 response.charset = charset
450 return response
450 return response
451
451
452 # in case we ever end up here
452 # in case we ever end up here
453 raise HTTPNotFound()
453 raise HTTPNotFound()
454
454
455 @LoginRequired()
455 @LoginRequired()
456 @HasRepoPermissionAnyDecorator(
456 @HasRepoPermissionAnyDecorator(
457 'repository.read', 'repository.write', 'repository.admin')
457 'repository.read', 'repository.write', 'repository.admin')
458 @view_config(
458 @view_config(
459 route_name='repo_files_diff_2way_redirect', request_method='GET',
459 route_name='repo_files_diff_2way_redirect', request_method='GET',
460 renderer=None)
460 renderer=None)
461 def repo_files_diff_2way_redirect(self):
461 def repo_files_diff_2way_redirect(self):
462 """
462 """
463 Kept only to make OLD links work
463 Kept only to make OLD links work
464 """
464 """
465 f_path = self._get_f_path_unchecked(self.request.matchdict)
465 f_path = self._get_f_path_unchecked(self.request.matchdict)
466 diff1 = self.request.GET.get('diff1', '')
466 diff1 = self.request.GET.get('diff1', '')
467 diff2 = self.request.GET.get('diff2', '')
467 diff2 = self.request.GET.get('diff2', '')
468
468
469 if not any((diff1, diff2)):
469 if not any((diff1, diff2)):
470 h.flash(
470 h.flash(
471 'Need query parameter "diff1" or "diff2" to generate a diff.',
471 'Need query parameter "diff1" or "diff2" to generate a diff.',
472 category='error')
472 category='error')
473 raise HTTPBadRequest()
473 raise HTTPBadRequest()
474
474
475 compare_url = h.route_path(
475 compare_url = h.route_path(
476 'repo_compare',
476 'repo_compare',
477 repo_name=self.db_repo_name,
477 repo_name=self.db_repo_name,
478 source_ref_type='rev',
478 source_ref_type='rev',
479 source_ref=diff1,
479 source_ref=diff1,
480 target_ref_type='rev',
480 target_ref_type='rev',
481 target_ref=diff2,
481 target_ref=diff2,
482 _query=dict(f_path=f_path, diffmode='sideside',
482 _query=dict(f_path=f_path, diffmode='sideside',
483 target_repo=self.db_repo_name,))
483 target_repo=self.db_repo_name,))
484 raise HTTPFound(compare_url)
484 raise HTTPFound(compare_url)
485
485
486 @LoginRequired()
486 @LoginRequired()
487 @HasRepoPermissionAnyDecorator(
487 @HasRepoPermissionAnyDecorator(
488 'repository.read', 'repository.write', 'repository.admin')
488 'repository.read', 'repository.write', 'repository.admin')
489 @view_config(
489 @view_config(
490 route_name='repo_files', request_method='GET',
490 route_name='repo_files', request_method='GET',
491 renderer=None)
491 renderer=None)
492 @view_config(
492 @view_config(
493 route_name='repo_files:default_path', request_method='GET',
493 route_name='repo_files:default_path', request_method='GET',
494 renderer=None)
494 renderer=None)
495 @view_config(
495 @view_config(
496 route_name='repo_files:default_commit', request_method='GET',
496 route_name='repo_files:default_commit', request_method='GET',
497 renderer=None)
497 renderer=None)
498 @view_config(
498 @view_config(
499 route_name='repo_files:rendered', request_method='GET',
499 route_name='repo_files:rendered', request_method='GET',
500 renderer=None)
500 renderer=None)
501 @view_config(
501 @view_config(
502 route_name='repo_files:annotated', request_method='GET',
502 route_name='repo_files:annotated', request_method='GET',
503 renderer=None)
503 renderer=None)
504 def repo_files(self):
504 def repo_files(self):
505 c = self.load_default_context()
505 c = self.load_default_context()
506
506
507 view_name = getattr(self.request.matched_route, 'name', None)
507 view_name = getattr(self.request.matched_route, 'name', None)
508
508
509 c.annotate = view_name == 'repo_files:annotated'
509 c.annotate = view_name == 'repo_files:annotated'
510 # default is false, but .rst/.md files later are auto rendered, we can
510 # default is false, but .rst/.md files later are auto rendered, we can
511 # overwrite auto rendering by setting this GET flag
511 # overwrite auto rendering by setting this GET flag
512 c.renderer = view_name == 'repo_files:rendered' or \
512 c.renderer = view_name == 'repo_files:rendered' or \
513 not self.request.GET.get('no-render', False)
513 not self.request.GET.get('no-render', False)
514
514
515 # redirect to given commit_id from form if given
515 # redirect to given commit_id from form if given
516 get_commit_id = self.request.GET.get('at_rev', None)
516 get_commit_id = self.request.GET.get('at_rev', None)
517 if get_commit_id:
517 if get_commit_id:
518 self._get_commit_or_redirect(get_commit_id)
518 self._get_commit_or_redirect(get_commit_id)
519
519
520 commit_id, f_path = self._get_commit_and_path()
520 commit_id, f_path = self._get_commit_and_path()
521 c.commit = self._get_commit_or_redirect(commit_id)
521 c.commit = self._get_commit_or_redirect(commit_id)
522 c.branch = self.request.GET.get('branch', None)
522 c.branch = self.request.GET.get('branch', None)
523 c.f_path = f_path
523 c.f_path = f_path
524
524
525 # prev link
525 # prev link
526 try:
526 try:
527 prev_commit = c.commit.prev(c.branch)
527 prev_commit = c.commit.prev(c.branch)
528 c.prev_commit = prev_commit
528 c.prev_commit = prev_commit
529 c.url_prev = h.route_path(
529 c.url_prev = h.route_path(
530 'repo_files', repo_name=self.db_repo_name,
530 'repo_files', repo_name=self.db_repo_name,
531 commit_id=prev_commit.raw_id, f_path=f_path)
531 commit_id=prev_commit.raw_id, f_path=f_path)
532 if c.branch:
532 if c.branch:
533 c.url_prev += '?branch=%s' % c.branch
533 c.url_prev += '?branch=%s' % c.branch
534 except (CommitDoesNotExistError, VCSError):
534 except (CommitDoesNotExistError, VCSError):
535 c.url_prev = '#'
535 c.url_prev = '#'
536 c.prev_commit = EmptyCommit()
536 c.prev_commit = EmptyCommit()
537
537
538 # next link
538 # next link
539 try:
539 try:
540 next_commit = c.commit.next(c.branch)
540 next_commit = c.commit.next(c.branch)
541 c.next_commit = next_commit
541 c.next_commit = next_commit
542 c.url_next = h.route_path(
542 c.url_next = h.route_path(
543 'repo_files', repo_name=self.db_repo_name,
543 'repo_files', repo_name=self.db_repo_name,
544 commit_id=next_commit.raw_id, f_path=f_path)
544 commit_id=next_commit.raw_id, f_path=f_path)
545 if c.branch:
545 if c.branch:
546 c.url_next += '?branch=%s' % c.branch
546 c.url_next += '?branch=%s' % c.branch
547 except (CommitDoesNotExistError, VCSError):
547 except (CommitDoesNotExistError, VCSError):
548 c.url_next = '#'
548 c.url_next = '#'
549 c.next_commit = EmptyCommit()
549 c.next_commit = EmptyCommit()
550
550
551 # files or dirs
551 # files or dirs
552 try:
552 try:
553 c.file = c.commit.get_node(f_path)
553 c.file = c.commit.get_node(f_path)
554 c.file_author = True
554 c.file_author = True
555 c.file_tree = ''
555 c.file_tree = ''
556
556
557 # load file content
557 # load file content
558 if c.file.is_file():
558 if c.file.is_file():
559 c.lf_node = c.file.get_largefile_node()
559 c.lf_node = c.file.get_largefile_node()
560
560
561 c.file_source_page = 'true'
561 c.file_source_page = 'true'
562 c.file_last_commit = c.file.last_commit
562 c.file_last_commit = c.file.last_commit
563 if c.file.size < c.visual.cut_off_limit_diff:
563 if c.file.size < c.visual.cut_off_limit_diff:
564 if c.annotate: # annotation has precedence over renderer
564 if c.annotate: # annotation has precedence over renderer
565 c.annotated_lines = filenode_as_annotated_lines_tokens(
565 c.annotated_lines = filenode_as_annotated_lines_tokens(
566 c.file
566 c.file
567 )
567 )
568 else:
568 else:
569 c.renderer = (
569 c.renderer = (
570 c.renderer and h.renderer_from_filename(c.file.path)
570 c.renderer and h.renderer_from_filename(c.file.path)
571 )
571 )
572 if not c.renderer:
572 if not c.renderer:
573 c.lines = filenode_as_lines_tokens(c.file)
573 c.lines = filenode_as_lines_tokens(c.file)
574
574
575 c.on_branch_head = self._is_valid_head(
575 c.on_branch_head = self._is_valid_head(
576 commit_id, self.rhodecode_vcs_repo)
576 commit_id, self.rhodecode_vcs_repo)
577
577
578 branch = c.commit.branch if (
578 branch = c.commit.branch if (
579 c.commit.branch and '/' not in c.commit.branch) else None
579 c.commit.branch and '/' not in c.commit.branch) else None
580 c.branch_or_raw_id = branch or c.commit.raw_id
580 c.branch_or_raw_id = branch or c.commit.raw_id
581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
581 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
582
582
583 author = c.file_last_commit.author
583 author = c.file_last_commit.author
584 c.authors = [[
584 c.authors = [[
585 h.email(author),
585 h.email(author),
586 h.person(author, 'username_or_name_or_email'),
586 h.person(author, 'username_or_name_or_email'),
587 1
587 1
588 ]]
588 ]]
589
589
590 else: # load tree content at path
590 else: # load tree content at path
591 c.file_source_page = 'false'
591 c.file_source_page = 'false'
592 c.authors = []
592 c.authors = []
593 # this loads a simple tree without metadata to speed things up
593 # this loads a simple tree without metadata to speed things up
594 # later via ajax we call repo_nodetree_full and fetch whole
594 # later via ajax we call repo_nodetree_full and fetch whole
595 c.file_tree = self._get_tree_at_commit(
595 c.file_tree = self._get_tree_at_commit(
596 c, c.commit.raw_id, f_path)
596 c, c.commit.raw_id, f_path)
597
597
598 except RepositoryError as e:
598 except RepositoryError as e:
599 h.flash(safe_str(h.escape(e)), category='error')
599 h.flash(safe_str(h.escape(e)), category='error')
600 raise HTTPNotFound()
600 raise HTTPNotFound()
601
601
602 if self.request.environ.get('HTTP_X_PJAX'):
602 if self.request.environ.get('HTTP_X_PJAX'):
603 html = render('rhodecode:templates/files/files_pjax.mako',
603 html = render('rhodecode:templates/files/files_pjax.mako',
604 self._get_template_context(c), self.request)
604 self._get_template_context(c), self.request)
605 else:
605 else:
606 html = render('rhodecode:templates/files/files.mako',
606 html = render('rhodecode:templates/files/files.mako',
607 self._get_template_context(c), self.request)
607 self._get_template_context(c), self.request)
608 return Response(html)
608 return Response(html)
609
609
610 @HasRepoPermissionAnyDecorator(
610 @HasRepoPermissionAnyDecorator(
611 'repository.read', 'repository.write', 'repository.admin')
611 'repository.read', 'repository.write', 'repository.admin')
612 @view_config(
612 @view_config(
613 route_name='repo_files:annotated_previous', request_method='GET',
613 route_name='repo_files:annotated_previous', request_method='GET',
614 renderer=None)
614 renderer=None)
615 def repo_files_annotated_previous(self):
615 def repo_files_annotated_previous(self):
616 self.load_default_context()
616 self.load_default_context()
617
617
618 commit_id, f_path = self._get_commit_and_path()
618 commit_id, f_path = self._get_commit_and_path()
619 commit = self._get_commit_or_redirect(commit_id)
619 commit = self._get_commit_or_redirect(commit_id)
620 prev_commit_id = commit.raw_id
620 prev_commit_id = commit.raw_id
621 line_anchor = self.request.GET.get('line_anchor')
621 line_anchor = self.request.GET.get('line_anchor')
622 is_file = False
622 is_file = False
623 try:
623 try:
624 _file = commit.get_node(f_path)
624 _file = commit.get_node(f_path)
625 is_file = _file.is_file()
625 is_file = _file.is_file()
626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
626 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
627 pass
627 pass
628
628
629 if is_file:
629 if is_file:
630 history = commit.get_file_history(f_path)
630 history = commit.get_file_history(f_path)
631 prev_commit_id = history[1].raw_id \
631 prev_commit_id = history[1].raw_id \
632 if len(history) > 1 else prev_commit_id
632 if len(history) > 1 else prev_commit_id
633 prev_url = h.route_path(
633 prev_url = h.route_path(
634 'repo_files:annotated', repo_name=self.db_repo_name,
634 'repo_files:annotated', repo_name=self.db_repo_name,
635 commit_id=prev_commit_id, f_path=f_path,
635 commit_id=prev_commit_id, f_path=f_path,
636 _anchor='L{}'.format(line_anchor))
636 _anchor='L{}'.format(line_anchor))
637
637
638 raise HTTPFound(prev_url)
638 raise HTTPFound(prev_url)
639
639
640 @LoginRequired()
640 @LoginRequired()
641 @HasRepoPermissionAnyDecorator(
641 @HasRepoPermissionAnyDecorator(
642 'repository.read', 'repository.write', 'repository.admin')
642 'repository.read', 'repository.write', 'repository.admin')
643 @view_config(
643 @view_config(
644 route_name='repo_nodetree_full', request_method='GET',
644 route_name='repo_nodetree_full', request_method='GET',
645 renderer=None, xhr=True)
645 renderer=None, xhr=True)
646 @view_config(
646 @view_config(
647 route_name='repo_nodetree_full:default_path', request_method='GET',
647 route_name='repo_nodetree_full:default_path', request_method='GET',
648 renderer=None, xhr=True)
648 renderer=None, xhr=True)
649 def repo_nodetree_full(self):
649 def repo_nodetree_full(self):
650 """
650 """
651 Returns rendered html of file tree that contains commit date,
651 Returns rendered html of file tree that contains commit date,
652 author, commit_id for the specified combination of
652 author, commit_id for the specified combination of
653 repo, commit_id and file path
653 repo, commit_id and file path
654 """
654 """
655 c = self.load_default_context()
655 c = self.load_default_context()
656
656
657 commit_id, f_path = self._get_commit_and_path()
657 commit_id, f_path = self._get_commit_and_path()
658 commit = self._get_commit_or_redirect(commit_id)
658 commit = self._get_commit_or_redirect(commit_id)
659 try:
659 try:
660 dir_node = commit.get_node(f_path)
660 dir_node = commit.get_node(f_path)
661 except RepositoryError as e:
661 except RepositoryError as e:
662 return Response('error: {}'.format(h.escape(safe_str(e))))
662 return Response('error: {}'.format(h.escape(safe_str(e))))
663
663
664 if dir_node.is_file():
664 if dir_node.is_file():
665 return Response('')
665 return Response('')
666
666
667 c.file = dir_node
667 c.file = dir_node
668 c.commit = commit
668 c.commit = commit
669
669
670 html = self._get_tree_at_commit(
670 html = self._get_tree_at_commit(
671 c, commit.raw_id, dir_node.path, full_load=True)
671 c, commit.raw_id, dir_node.path, full_load=True)
672
672
673 return Response(html)
673 return Response(html)
674
674
675 def _get_attachement_disposition(self, f_path):
675 def _get_attachement_disposition(self, f_path):
676 return 'attachment; filename=%s' % \
676 return 'attachment; filename=%s' % \
677 safe_str(f_path.split(Repository.NAME_SEP)[-1])
677 safe_str(f_path.split(Repository.NAME_SEP)[-1])
678
678
679 @LoginRequired()
679 @LoginRequired()
680 @HasRepoPermissionAnyDecorator(
680 @HasRepoPermissionAnyDecorator(
681 'repository.read', 'repository.write', 'repository.admin')
681 'repository.read', 'repository.write', 'repository.admin')
682 @view_config(
682 @view_config(
683 route_name='repo_file_raw', request_method='GET',
683 route_name='repo_file_raw', request_method='GET',
684 renderer=None)
684 renderer=None)
685 def repo_file_raw(self):
685 def repo_file_raw(self):
686 """
686 """
687 Action for show as raw, some mimetypes are "rendered",
687 Action for show as raw, some mimetypes are "rendered",
688 those include images, icons.
688 those include images, icons.
689 """
689 """
690 c = self.load_default_context()
690 c = self.load_default_context()
691
691
692 commit_id, f_path = self._get_commit_and_path()
692 commit_id, f_path = self._get_commit_and_path()
693 commit = self._get_commit_or_redirect(commit_id)
693 commit = self._get_commit_or_redirect(commit_id)
694 file_node = self._get_filenode_or_redirect(commit, f_path)
694 file_node = self._get_filenode_or_redirect(commit, f_path)
695
695
696 raw_mimetype_mapping = {
696 raw_mimetype_mapping = {
697 # map original mimetype to a mimetype used for "show as raw"
697 # map original mimetype to a mimetype used for "show as raw"
698 # you can also provide a content-disposition to override the
698 # you can also provide a content-disposition to override the
699 # default "attachment" disposition.
699 # default "attachment" disposition.
700 # orig_type: (new_type, new_dispo)
700 # orig_type: (new_type, new_dispo)
701
701
702 # show images inline:
702 # show images inline:
703 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
703 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
704 # for example render an SVG with javascript inside or even render
704 # for example render an SVG with javascript inside or even render
705 # HTML.
705 # HTML.
706 'image/x-icon': ('image/x-icon', 'inline'),
706 'image/x-icon': ('image/x-icon', 'inline'),
707 'image/png': ('image/png', 'inline'),
707 'image/png': ('image/png', 'inline'),
708 'image/gif': ('image/gif', 'inline'),
708 'image/gif': ('image/gif', 'inline'),
709 'image/jpeg': ('image/jpeg', 'inline'),
709 'image/jpeg': ('image/jpeg', 'inline'),
710 'application/pdf': ('application/pdf', 'inline'),
710 'application/pdf': ('application/pdf', 'inline'),
711 }
711 }
712
712
713 mimetype = file_node.mimetype
713 mimetype = file_node.mimetype
714 try:
714 try:
715 mimetype, disposition = raw_mimetype_mapping[mimetype]
715 mimetype, disposition = raw_mimetype_mapping[mimetype]
716 except KeyError:
716 except KeyError:
717 # we don't know anything special about this, handle it safely
717 # we don't know anything special about this, handle it safely
718 if file_node.is_binary:
718 if file_node.is_binary:
719 # do same as download raw for binary files
719 # do same as download raw for binary files
720 mimetype, disposition = 'application/octet-stream', 'attachment'
720 mimetype, disposition = 'application/octet-stream', 'attachment'
721 else:
721 else:
722 # do not just use the original mimetype, but force text/plain,
722 # do not just use the original mimetype, but force text/plain,
723 # otherwise it would serve text/html and that might be unsafe.
723 # otherwise it would serve text/html and that might be unsafe.
724 # Note: underlying vcs library fakes text/plain mimetype if the
724 # Note: underlying vcs library fakes text/plain mimetype if the
725 # mimetype can not be determined and it thinks it is not
725 # mimetype can not be determined and it thinks it is not
726 # binary.This might lead to erroneous text display in some
726 # binary.This might lead to erroneous text display in some
727 # cases, but helps in other cases, like with text files
727 # cases, but helps in other cases, like with text files
728 # without extension.
728 # without extension.
729 mimetype, disposition = 'text/plain', 'inline'
729 mimetype, disposition = 'text/plain', 'inline'
730
730
731 if disposition == 'attachment':
731 if disposition == 'attachment':
732 disposition = self._get_attachement_disposition(f_path)
732 disposition = self._get_attachement_disposition(f_path)
733
733
734 def stream_node():
734 def stream_node():
735 yield file_node.raw_bytes
735 yield file_node.raw_bytes
736
736
737 response = Response(app_iter=stream_node())
737 response = Response(app_iter=stream_node())
738 response.content_disposition = disposition
738 response.content_disposition = disposition
739 response.content_type = mimetype
739 response.content_type = mimetype
740
740
741 charset = self._get_default_encoding(c)
741 charset = self._get_default_encoding(c)
742 if charset:
742 if charset:
743 response.charset = charset
743 response.charset = charset
744
744
745 return response
745 return response
746
746
747 @LoginRequired()
747 @LoginRequired()
748 @HasRepoPermissionAnyDecorator(
748 @HasRepoPermissionAnyDecorator(
749 'repository.read', 'repository.write', 'repository.admin')
749 'repository.read', 'repository.write', 'repository.admin')
750 @view_config(
750 @view_config(
751 route_name='repo_file_download', request_method='GET',
751 route_name='repo_file_download', request_method='GET',
752 renderer=None)
752 renderer=None)
753 @view_config(
753 @view_config(
754 route_name='repo_file_download:legacy', request_method='GET',
754 route_name='repo_file_download:legacy', request_method='GET',
755 renderer=None)
755 renderer=None)
756 def repo_file_download(self):
756 def repo_file_download(self):
757 c = self.load_default_context()
757 c = self.load_default_context()
758
758
759 commit_id, f_path = self._get_commit_and_path()
759 commit_id, f_path = self._get_commit_and_path()
760 commit = self._get_commit_or_redirect(commit_id)
760 commit = self._get_commit_or_redirect(commit_id)
761 file_node = self._get_filenode_or_redirect(commit, f_path)
761 file_node = self._get_filenode_or_redirect(commit, f_path)
762
762
763 if self.request.GET.get('lf'):
763 if self.request.GET.get('lf'):
764 # only if lf get flag is passed, we download this file
764 # only if lf get flag is passed, we download this file
765 # as LFS/Largefile
765 # as LFS/Largefile
766 lf_node = file_node.get_largefile_node()
766 lf_node = file_node.get_largefile_node()
767 if lf_node:
767 if lf_node:
768 # overwrite our pointer with the REAL large-file
768 # overwrite our pointer with the REAL large-file
769 file_node = lf_node
769 file_node = lf_node
770
770
771 disposition = self._get_attachement_disposition(f_path)
771 disposition = self._get_attachement_disposition(f_path)
772
772
773 def stream_node():
773 def stream_node():
774 yield file_node.raw_bytes
774 yield file_node.raw_bytes
775
775
776 response = Response(app_iter=stream_node())
776 response = Response(app_iter=stream_node())
777 response.content_disposition = disposition
777 response.content_disposition = disposition
778 response.content_type = file_node.mimetype
778 response.content_type = file_node.mimetype
779
779
780 charset = self._get_default_encoding(c)
780 charset = self._get_default_encoding(c)
781 if charset:
781 if charset:
782 response.charset = charset
782 response.charset = charset
783
783
784 return response
784 return response
785
785
786 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
786 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
787
787
788 cache_seconds = safe_int(
788 cache_seconds = safe_int(
789 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
789 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
790 cache_on = cache_seconds > 0
790 cache_on = cache_seconds > 0
791 log.debug(
791 log.debug(
792 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
792 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
793 'with caching: %s[TTL: %ss]' % (
793 'with caching: %s[TTL: %ss]' % (
794 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
794 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
795
795
796 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
796 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
797 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
797 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
798
798
799 @region.cache_on_arguments(namespace=cache_namespace_uid,
799 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
800 should_cache_fn=lambda v: cache_on)
800 condition=cache_on)
801 def compute_file_search(repo_id, commit_id, f_path):
801 def compute_file_search(repo_id, commit_id, f_path):
802 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
802 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
803 repo_id, commit_id, f_path)
803 repo_id, commit_id, f_path)
804 try:
804 try:
805 _d, _f = ScmModel().get_nodes(
805 _d, _f = ScmModel().get_nodes(
806 repo_name, commit_id, f_path, flat=False)
806 repo_name, commit_id, f_path, flat=False)
807 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
807 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
808 log.exception(safe_str(e))
808 log.exception(safe_str(e))
809 h.flash(safe_str(h.escape(e)), category='error')
809 h.flash(safe_str(h.escape(e)), category='error')
810 raise HTTPFound(h.route_path(
810 raise HTTPFound(h.route_path(
811 'repo_files', repo_name=self.db_repo_name,
811 'repo_files', repo_name=self.db_repo_name,
812 commit_id='tip', f_path='/'))
812 commit_id='tip', f_path='/'))
813 return _d + _f
813 return _d + _f
814
814
815 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
815 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
816
816
817 @LoginRequired()
817 @LoginRequired()
818 @HasRepoPermissionAnyDecorator(
818 @HasRepoPermissionAnyDecorator(
819 'repository.read', 'repository.write', 'repository.admin')
819 'repository.read', 'repository.write', 'repository.admin')
820 @view_config(
820 @view_config(
821 route_name='repo_files_nodelist', request_method='GET',
821 route_name='repo_files_nodelist', request_method='GET',
822 renderer='json_ext', xhr=True)
822 renderer='json_ext', xhr=True)
823 def repo_nodelist(self):
823 def repo_nodelist(self):
824 self.load_default_context()
824 self.load_default_context()
825
825
826 commit_id, f_path = self._get_commit_and_path()
826 commit_id, f_path = self._get_commit_and_path()
827 commit = self._get_commit_or_redirect(commit_id)
827 commit = self._get_commit_or_redirect(commit_id)
828
828
829 metadata = self._get_nodelist_at_commit(
829 metadata = self._get_nodelist_at_commit(
830 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
830 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
831 return {'nodes': metadata}
831 return {'nodes': metadata}
832
832
833 def _create_references(
833 def _create_references(
834 self, branches_or_tags, symbolic_reference, f_path):
834 self, branches_or_tags, symbolic_reference, f_path):
835 items = []
835 items = []
836 for name, commit_id in branches_or_tags.items():
836 for name, commit_id in branches_or_tags.items():
837 sym_ref = symbolic_reference(commit_id, name, f_path)
837 sym_ref = symbolic_reference(commit_id, name, f_path)
838 items.append((sym_ref, name))
838 items.append((sym_ref, name))
839 return items
839 return items
840
840
841 def _symbolic_reference(self, commit_id, name, f_path):
841 def _symbolic_reference(self, commit_id, name, f_path):
842 return commit_id
842 return commit_id
843
843
844 def _symbolic_reference_svn(self, commit_id, name, f_path):
844 def _symbolic_reference_svn(self, commit_id, name, f_path):
845 new_f_path = vcspath.join(name, f_path)
845 new_f_path = vcspath.join(name, f_path)
846 return u'%s@%s' % (new_f_path, commit_id)
846 return u'%s@%s' % (new_f_path, commit_id)
847
847
848 def _get_node_history(self, commit_obj, f_path, commits=None):
848 def _get_node_history(self, commit_obj, f_path, commits=None):
849 """
849 """
850 get commit history for given node
850 get commit history for given node
851
851
852 :param commit_obj: commit to calculate history
852 :param commit_obj: commit to calculate history
853 :param f_path: path for node to calculate history for
853 :param f_path: path for node to calculate history for
854 :param commits: if passed don't calculate history and take
854 :param commits: if passed don't calculate history and take
855 commits defined in this list
855 commits defined in this list
856 """
856 """
857 _ = self.request.translate
857 _ = self.request.translate
858
858
859 # calculate history based on tip
859 # calculate history based on tip
860 tip = self.rhodecode_vcs_repo.get_commit()
860 tip = self.rhodecode_vcs_repo.get_commit()
861 if commits is None:
861 if commits is None:
862 pre_load = ["author", "branch"]
862 pre_load = ["author", "branch"]
863 try:
863 try:
864 commits = tip.get_file_history(f_path, pre_load=pre_load)
864 commits = tip.get_file_history(f_path, pre_load=pre_load)
865 except (NodeDoesNotExistError, CommitError):
865 except (NodeDoesNotExistError, CommitError):
866 # this node is not present at tip!
866 # this node is not present at tip!
867 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
867 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
868
868
869 history = []
869 history = []
870 commits_group = ([], _("Changesets"))
870 commits_group = ([], _("Changesets"))
871 for commit in commits:
871 for commit in commits:
872 branch = ' (%s)' % commit.branch if commit.branch else ''
872 branch = ' (%s)' % commit.branch if commit.branch else ''
873 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
873 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
874 commits_group[0].append((commit.raw_id, n_desc,))
874 commits_group[0].append((commit.raw_id, n_desc,))
875 history.append(commits_group)
875 history.append(commits_group)
876
876
877 symbolic_reference = self._symbolic_reference
877 symbolic_reference = self._symbolic_reference
878
878
879 if self.rhodecode_vcs_repo.alias == 'svn':
879 if self.rhodecode_vcs_repo.alias == 'svn':
880 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
880 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
881 f_path, self.rhodecode_vcs_repo)
881 f_path, self.rhodecode_vcs_repo)
882 if adjusted_f_path != f_path:
882 if adjusted_f_path != f_path:
883 log.debug(
883 log.debug(
884 'Recognized svn tag or branch in file "%s", using svn '
884 'Recognized svn tag or branch in file "%s", using svn '
885 'specific symbolic references', f_path)
885 'specific symbolic references', f_path)
886 f_path = adjusted_f_path
886 f_path = adjusted_f_path
887 symbolic_reference = self._symbolic_reference_svn
887 symbolic_reference = self._symbolic_reference_svn
888
888
889 branches = self._create_references(
889 branches = self._create_references(
890 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
890 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
891 branches_group = (branches, _("Branches"))
891 branches_group = (branches, _("Branches"))
892
892
893 tags = self._create_references(
893 tags = self._create_references(
894 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
894 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
895 tags_group = (tags, _("Tags"))
895 tags_group = (tags, _("Tags"))
896
896
897 history.append(branches_group)
897 history.append(branches_group)
898 history.append(tags_group)
898 history.append(tags_group)
899
899
900 return history, commits
900 return history, commits
901
901
902 @LoginRequired()
902 @LoginRequired()
903 @HasRepoPermissionAnyDecorator(
903 @HasRepoPermissionAnyDecorator(
904 'repository.read', 'repository.write', 'repository.admin')
904 'repository.read', 'repository.write', 'repository.admin')
905 @view_config(
905 @view_config(
906 route_name='repo_file_history', request_method='GET',
906 route_name='repo_file_history', request_method='GET',
907 renderer='json_ext')
907 renderer='json_ext')
908 def repo_file_history(self):
908 def repo_file_history(self):
909 self.load_default_context()
909 self.load_default_context()
910
910
911 commit_id, f_path = self._get_commit_and_path()
911 commit_id, f_path = self._get_commit_and_path()
912 commit = self._get_commit_or_redirect(commit_id)
912 commit = self._get_commit_or_redirect(commit_id)
913 file_node = self._get_filenode_or_redirect(commit, f_path)
913 file_node = self._get_filenode_or_redirect(commit, f_path)
914
914
915 if file_node.is_file():
915 if file_node.is_file():
916 file_history, _hist = self._get_node_history(commit, f_path)
916 file_history, _hist = self._get_node_history(commit, f_path)
917
917
918 res = []
918 res = []
919 for obj in file_history:
919 for obj in file_history:
920 res.append({
920 res.append({
921 'text': obj[1],
921 'text': obj[1],
922 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
922 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
923 })
923 })
924
924
925 data = {
925 data = {
926 'more': False,
926 'more': False,
927 'results': res
927 'results': res
928 }
928 }
929 return data
929 return data
930
930
931 log.warning('Cannot fetch history for directory')
931 log.warning('Cannot fetch history for directory')
932 raise HTTPBadRequest()
932 raise HTTPBadRequest()
933
933
934 @LoginRequired()
934 @LoginRequired()
935 @HasRepoPermissionAnyDecorator(
935 @HasRepoPermissionAnyDecorator(
936 'repository.read', 'repository.write', 'repository.admin')
936 'repository.read', 'repository.write', 'repository.admin')
937 @view_config(
937 @view_config(
938 route_name='repo_file_authors', request_method='GET',
938 route_name='repo_file_authors', request_method='GET',
939 renderer='rhodecode:templates/files/file_authors_box.mako')
939 renderer='rhodecode:templates/files/file_authors_box.mako')
940 def repo_file_authors(self):
940 def repo_file_authors(self):
941 c = self.load_default_context()
941 c = self.load_default_context()
942
942
943 commit_id, f_path = self._get_commit_and_path()
943 commit_id, f_path = self._get_commit_and_path()
944 commit = self._get_commit_or_redirect(commit_id)
944 commit = self._get_commit_or_redirect(commit_id)
945 file_node = self._get_filenode_or_redirect(commit, f_path)
945 file_node = self._get_filenode_or_redirect(commit, f_path)
946
946
947 if not file_node.is_file():
947 if not file_node.is_file():
948 raise HTTPBadRequest()
948 raise HTTPBadRequest()
949
949
950 c.file_last_commit = file_node.last_commit
950 c.file_last_commit = file_node.last_commit
951 if self.request.GET.get('annotate') == '1':
951 if self.request.GET.get('annotate') == '1':
952 # use _hist from annotation if annotation mode is on
952 # use _hist from annotation if annotation mode is on
953 commit_ids = set(x[1] for x in file_node.annotate)
953 commit_ids = set(x[1] for x in file_node.annotate)
954 _hist = (
954 _hist = (
955 self.rhodecode_vcs_repo.get_commit(commit_id)
955 self.rhodecode_vcs_repo.get_commit(commit_id)
956 for commit_id in commit_ids)
956 for commit_id in commit_ids)
957 else:
957 else:
958 _f_history, _hist = self._get_node_history(commit, f_path)
958 _f_history, _hist = self._get_node_history(commit, f_path)
959 c.file_author = False
959 c.file_author = False
960
960
961 unique = collections.OrderedDict()
961 unique = collections.OrderedDict()
962 for commit in _hist:
962 for commit in _hist:
963 author = commit.author
963 author = commit.author
964 if author not in unique:
964 if author not in unique:
965 unique[commit.author] = [
965 unique[commit.author] = [
966 h.email(author),
966 h.email(author),
967 h.person(author, 'username_or_name_or_email'),
967 h.person(author, 'username_or_name_or_email'),
968 1 # counter
968 1 # counter
969 ]
969 ]
970
970
971 else:
971 else:
972 # increase counter
972 # increase counter
973 unique[commit.author][2] += 1
973 unique[commit.author][2] += 1
974
974
975 c.authors = [val for val in unique.values()]
975 c.authors = [val for val in unique.values()]
976
976
977 return self._get_template_context(c)
977 return self._get_template_context(c)
978
978
979 @LoginRequired()
979 @LoginRequired()
980 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
980 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
981 @view_config(
981 @view_config(
982 route_name='repo_files_remove_file', request_method='GET',
982 route_name='repo_files_remove_file', request_method='GET',
983 renderer='rhodecode:templates/files/files_delete.mako')
983 renderer='rhodecode:templates/files/files_delete.mako')
984 def repo_files_remove_file(self):
984 def repo_files_remove_file(self):
985 _ = self.request.translate
985 _ = self.request.translate
986 c = self.load_default_context()
986 c = self.load_default_context()
987 commit_id, f_path = self._get_commit_and_path()
987 commit_id, f_path = self._get_commit_and_path()
988
988
989 self._ensure_not_locked()
989 self._ensure_not_locked()
990
990
991 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
991 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
992 h.flash(_('You can only delete files with commit '
992 h.flash(_('You can only delete files with commit '
993 'being a valid branch '), category='warning')
993 'being a valid branch '), category='warning')
994 raise HTTPFound(
994 raise HTTPFound(
995 h.route_path('repo_files',
995 h.route_path('repo_files',
996 repo_name=self.db_repo_name, commit_id='tip',
996 repo_name=self.db_repo_name, commit_id='tip',
997 f_path=f_path))
997 f_path=f_path))
998
998
999 c.commit = self._get_commit_or_redirect(commit_id)
999 c.commit = self._get_commit_or_redirect(commit_id)
1000 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1000 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1001
1001
1002 c.default_message = _(
1002 c.default_message = _(
1003 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1003 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1004 c.f_path = f_path
1004 c.f_path = f_path
1005
1005
1006 return self._get_template_context(c)
1006 return self._get_template_context(c)
1007
1007
1008 @LoginRequired()
1008 @LoginRequired()
1009 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1009 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1010 @CSRFRequired()
1010 @CSRFRequired()
1011 @view_config(
1011 @view_config(
1012 route_name='repo_files_delete_file', request_method='POST',
1012 route_name='repo_files_delete_file', request_method='POST',
1013 renderer=None)
1013 renderer=None)
1014 def repo_files_delete_file(self):
1014 def repo_files_delete_file(self):
1015 _ = self.request.translate
1015 _ = self.request.translate
1016
1016
1017 c = self.load_default_context()
1017 c = self.load_default_context()
1018 commit_id, f_path = self._get_commit_and_path()
1018 commit_id, f_path = self._get_commit_and_path()
1019
1019
1020 self._ensure_not_locked()
1020 self._ensure_not_locked()
1021
1021
1022 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1022 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1023 h.flash(_('You can only delete files with commit '
1023 h.flash(_('You can only delete files with commit '
1024 'being a valid branch '), category='warning')
1024 'being a valid branch '), category='warning')
1025 raise HTTPFound(
1025 raise HTTPFound(
1026 h.route_path('repo_files',
1026 h.route_path('repo_files',
1027 repo_name=self.db_repo_name, commit_id='tip',
1027 repo_name=self.db_repo_name, commit_id='tip',
1028 f_path=f_path))
1028 f_path=f_path))
1029
1029
1030 c.commit = self._get_commit_or_redirect(commit_id)
1030 c.commit = self._get_commit_or_redirect(commit_id)
1031 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1031 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1032
1032
1033 c.default_message = _(
1033 c.default_message = _(
1034 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1034 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1035 c.f_path = f_path
1035 c.f_path = f_path
1036 node_path = f_path
1036 node_path = f_path
1037 author = self._rhodecode_db_user.full_contact
1037 author = self._rhodecode_db_user.full_contact
1038 message = self.request.POST.get('message') or c.default_message
1038 message = self.request.POST.get('message') or c.default_message
1039 try:
1039 try:
1040 nodes = {
1040 nodes = {
1041 node_path: {
1041 node_path: {
1042 'content': ''
1042 'content': ''
1043 }
1043 }
1044 }
1044 }
1045 ScmModel().delete_nodes(
1045 ScmModel().delete_nodes(
1046 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1046 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1047 message=message,
1047 message=message,
1048 nodes=nodes,
1048 nodes=nodes,
1049 parent_commit=c.commit,
1049 parent_commit=c.commit,
1050 author=author,
1050 author=author,
1051 )
1051 )
1052
1052
1053 h.flash(
1053 h.flash(
1054 _('Successfully deleted file `{}`').format(
1054 _('Successfully deleted file `{}`').format(
1055 h.escape(f_path)), category='success')
1055 h.escape(f_path)), category='success')
1056 except Exception:
1056 except Exception:
1057 log.exception('Error during commit operation')
1057 log.exception('Error during commit operation')
1058 h.flash(_('Error occurred during commit'), category='error')
1058 h.flash(_('Error occurred during commit'), category='error')
1059 raise HTTPFound(
1059 raise HTTPFound(
1060 h.route_path('repo_commit', repo_name=self.db_repo_name,
1060 h.route_path('repo_commit', repo_name=self.db_repo_name,
1061 commit_id='tip'))
1061 commit_id='tip'))
1062
1062
1063 @LoginRequired()
1063 @LoginRequired()
1064 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1064 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1065 @view_config(
1065 @view_config(
1066 route_name='repo_files_edit_file', request_method='GET',
1066 route_name='repo_files_edit_file', request_method='GET',
1067 renderer='rhodecode:templates/files/files_edit.mako')
1067 renderer='rhodecode:templates/files/files_edit.mako')
1068 def repo_files_edit_file(self):
1068 def repo_files_edit_file(self):
1069 _ = self.request.translate
1069 _ = self.request.translate
1070 c = self.load_default_context()
1070 c = self.load_default_context()
1071 commit_id, f_path = self._get_commit_and_path()
1071 commit_id, f_path = self._get_commit_and_path()
1072
1072
1073 self._ensure_not_locked()
1073 self._ensure_not_locked()
1074
1074
1075 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1075 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1076 h.flash(_('You can only edit files with commit '
1076 h.flash(_('You can only edit files with commit '
1077 'being a valid branch '), category='warning')
1077 'being a valid branch '), category='warning')
1078 raise HTTPFound(
1078 raise HTTPFound(
1079 h.route_path('repo_files',
1079 h.route_path('repo_files',
1080 repo_name=self.db_repo_name, commit_id='tip',
1080 repo_name=self.db_repo_name, commit_id='tip',
1081 f_path=f_path))
1081 f_path=f_path))
1082
1082
1083 c.commit = self._get_commit_or_redirect(commit_id)
1083 c.commit = self._get_commit_or_redirect(commit_id)
1084 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1084 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1085
1085
1086 if c.file.is_binary:
1086 if c.file.is_binary:
1087 files_url = h.route_path(
1087 files_url = h.route_path(
1088 'repo_files',
1088 'repo_files',
1089 repo_name=self.db_repo_name,
1089 repo_name=self.db_repo_name,
1090 commit_id=c.commit.raw_id, f_path=f_path)
1090 commit_id=c.commit.raw_id, f_path=f_path)
1091 raise HTTPFound(files_url)
1091 raise HTTPFound(files_url)
1092
1092
1093 c.default_message = _(
1093 c.default_message = _(
1094 'Edited file {} via RhodeCode Enterprise').format(f_path)
1094 'Edited file {} via RhodeCode Enterprise').format(f_path)
1095 c.f_path = f_path
1095 c.f_path = f_path
1096
1096
1097 return self._get_template_context(c)
1097 return self._get_template_context(c)
1098
1098
1099 @LoginRequired()
1099 @LoginRequired()
1100 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1100 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1101 @CSRFRequired()
1101 @CSRFRequired()
1102 @view_config(
1102 @view_config(
1103 route_name='repo_files_update_file', request_method='POST',
1103 route_name='repo_files_update_file', request_method='POST',
1104 renderer=None)
1104 renderer=None)
1105 def repo_files_update_file(self):
1105 def repo_files_update_file(self):
1106 _ = self.request.translate
1106 _ = self.request.translate
1107 c = self.load_default_context()
1107 c = self.load_default_context()
1108 commit_id, f_path = self._get_commit_and_path()
1108 commit_id, f_path = self._get_commit_and_path()
1109
1109
1110 self._ensure_not_locked()
1110 self._ensure_not_locked()
1111
1111
1112 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1112 if not self._is_valid_head(commit_id, self.rhodecode_vcs_repo):
1113 h.flash(_('You can only edit files with commit '
1113 h.flash(_('You can only edit files with commit '
1114 'being a valid branch '), category='warning')
1114 'being a valid branch '), category='warning')
1115 raise HTTPFound(
1115 raise HTTPFound(
1116 h.route_path('repo_files',
1116 h.route_path('repo_files',
1117 repo_name=self.db_repo_name, commit_id='tip',
1117 repo_name=self.db_repo_name, commit_id='tip',
1118 f_path=f_path))
1118 f_path=f_path))
1119
1119
1120 c.commit = self._get_commit_or_redirect(commit_id)
1120 c.commit = self._get_commit_or_redirect(commit_id)
1121 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1121 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1122
1122
1123 if c.file.is_binary:
1123 if c.file.is_binary:
1124 raise HTTPFound(
1124 raise HTTPFound(
1125 h.route_path('repo_files',
1125 h.route_path('repo_files',
1126 repo_name=self.db_repo_name,
1126 repo_name=self.db_repo_name,
1127 commit_id=c.commit.raw_id,
1127 commit_id=c.commit.raw_id,
1128 f_path=f_path))
1128 f_path=f_path))
1129
1129
1130 c.default_message = _(
1130 c.default_message = _(
1131 'Edited file {} via RhodeCode Enterprise').format(f_path)
1131 'Edited file {} via RhodeCode Enterprise').format(f_path)
1132 c.f_path = f_path
1132 c.f_path = f_path
1133 old_content = c.file.content
1133 old_content = c.file.content
1134 sl = old_content.splitlines(1)
1134 sl = old_content.splitlines(1)
1135 first_line = sl[0] if sl else ''
1135 first_line = sl[0] if sl else ''
1136
1136
1137 r_post = self.request.POST
1137 r_post = self.request.POST
1138 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1138 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1139 mode = detect_mode(first_line, 0)
1139 mode = detect_mode(first_line, 0)
1140 content = convert_line_endings(r_post.get('content', ''), mode)
1140 content = convert_line_endings(r_post.get('content', ''), mode)
1141
1141
1142 message = r_post.get('message') or c.default_message
1142 message = r_post.get('message') or c.default_message
1143 org_f_path = c.file.unicode_path
1143 org_f_path = c.file.unicode_path
1144 filename = r_post['filename']
1144 filename = r_post['filename']
1145 org_filename = c.file.name
1145 org_filename = c.file.name
1146
1146
1147 if content == old_content and filename == org_filename:
1147 if content == old_content and filename == org_filename:
1148 h.flash(_('No changes'), category='warning')
1148 h.flash(_('No changes'), category='warning')
1149 raise HTTPFound(
1149 raise HTTPFound(
1150 h.route_path('repo_commit', repo_name=self.db_repo_name,
1150 h.route_path('repo_commit', repo_name=self.db_repo_name,
1151 commit_id='tip'))
1151 commit_id='tip'))
1152 try:
1152 try:
1153 mapping = {
1153 mapping = {
1154 org_f_path: {
1154 org_f_path: {
1155 'org_filename': org_f_path,
1155 'org_filename': org_f_path,
1156 'filename': os.path.join(c.file.dir_path, filename),
1156 'filename': os.path.join(c.file.dir_path, filename),
1157 'content': content,
1157 'content': content,
1158 'lexer': '',
1158 'lexer': '',
1159 'op': 'mod',
1159 'op': 'mod',
1160 }
1160 }
1161 }
1161 }
1162
1162
1163 ScmModel().update_nodes(
1163 ScmModel().update_nodes(
1164 user=self._rhodecode_db_user.user_id,
1164 user=self._rhodecode_db_user.user_id,
1165 repo=self.db_repo,
1165 repo=self.db_repo,
1166 message=message,
1166 message=message,
1167 nodes=mapping,
1167 nodes=mapping,
1168 parent_commit=c.commit,
1168 parent_commit=c.commit,
1169 )
1169 )
1170
1170
1171 h.flash(
1171 h.flash(
1172 _('Successfully committed changes to file `{}`').format(
1172 _('Successfully committed changes to file `{}`').format(
1173 h.escape(f_path)), category='success')
1173 h.escape(f_path)), category='success')
1174 except Exception:
1174 except Exception:
1175 log.exception('Error occurred during commit')
1175 log.exception('Error occurred during commit')
1176 h.flash(_('Error occurred during commit'), category='error')
1176 h.flash(_('Error occurred during commit'), category='error')
1177 raise HTTPFound(
1177 raise HTTPFound(
1178 h.route_path('repo_commit', repo_name=self.db_repo_name,
1178 h.route_path('repo_commit', repo_name=self.db_repo_name,
1179 commit_id='tip'))
1179 commit_id='tip'))
1180
1180
1181 @LoginRequired()
1181 @LoginRequired()
1182 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1182 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1183 @view_config(
1183 @view_config(
1184 route_name='repo_files_add_file', request_method='GET',
1184 route_name='repo_files_add_file', request_method='GET',
1185 renderer='rhodecode:templates/files/files_add.mako')
1185 renderer='rhodecode:templates/files/files_add.mako')
1186 def repo_files_add_file(self):
1186 def repo_files_add_file(self):
1187 _ = self.request.translate
1187 _ = self.request.translate
1188 c = self.load_default_context()
1188 c = self.load_default_context()
1189 commit_id, f_path = self._get_commit_and_path()
1189 commit_id, f_path = self._get_commit_and_path()
1190
1190
1191 self._ensure_not_locked()
1191 self._ensure_not_locked()
1192
1192
1193 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1193 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1194 if c.commit is None:
1194 if c.commit is None:
1195 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1195 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1196 c.default_message = (_('Added file via RhodeCode Enterprise'))
1196 c.default_message = (_('Added file via RhodeCode Enterprise'))
1197 c.f_path = f_path.lstrip('/') # ensure not relative path
1197 c.f_path = f_path.lstrip('/') # ensure not relative path
1198
1198
1199 return self._get_template_context(c)
1199 return self._get_template_context(c)
1200
1200
1201 @LoginRequired()
1201 @LoginRequired()
1202 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1202 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1203 @CSRFRequired()
1203 @CSRFRequired()
1204 @view_config(
1204 @view_config(
1205 route_name='repo_files_create_file', request_method='POST',
1205 route_name='repo_files_create_file', request_method='POST',
1206 renderer=None)
1206 renderer=None)
1207 def repo_files_create_file(self):
1207 def repo_files_create_file(self):
1208 _ = self.request.translate
1208 _ = self.request.translate
1209 c = self.load_default_context()
1209 c = self.load_default_context()
1210 commit_id, f_path = self._get_commit_and_path()
1210 commit_id, f_path = self._get_commit_and_path()
1211
1211
1212 self._ensure_not_locked()
1212 self._ensure_not_locked()
1213
1213
1214 r_post = self.request.POST
1214 r_post = self.request.POST
1215
1215
1216 c.commit = self._get_commit_or_redirect(
1216 c.commit = self._get_commit_or_redirect(
1217 commit_id, redirect_after=False)
1217 commit_id, redirect_after=False)
1218 if c.commit is None:
1218 if c.commit is None:
1219 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1219 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1220 c.default_message = (_('Added file via RhodeCode Enterprise'))
1220 c.default_message = (_('Added file via RhodeCode Enterprise'))
1221 c.f_path = f_path
1221 c.f_path = f_path
1222 unix_mode = 0
1222 unix_mode = 0
1223 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1223 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1224
1224
1225 message = r_post.get('message') or c.default_message
1225 message = r_post.get('message') or c.default_message
1226 filename = r_post.get('filename')
1226 filename = r_post.get('filename')
1227 location = r_post.get('location', '') # dir location
1227 location = r_post.get('location', '') # dir location
1228 file_obj = r_post.get('upload_file', None)
1228 file_obj = r_post.get('upload_file', None)
1229
1229
1230 if file_obj is not None and hasattr(file_obj, 'filename'):
1230 if file_obj is not None and hasattr(file_obj, 'filename'):
1231 filename = r_post.get('filename_upload')
1231 filename = r_post.get('filename_upload')
1232 content = file_obj.file
1232 content = file_obj.file
1233
1233
1234 if hasattr(content, 'file'):
1234 if hasattr(content, 'file'):
1235 # non posix systems store real file under file attr
1235 # non posix systems store real file under file attr
1236 content = content.file
1236 content = content.file
1237
1237
1238 if self.rhodecode_vcs_repo.is_empty:
1238 if self.rhodecode_vcs_repo.is_empty:
1239 default_redirect_url = h.route_path(
1239 default_redirect_url = h.route_path(
1240 'repo_summary', repo_name=self.db_repo_name)
1240 'repo_summary', repo_name=self.db_repo_name)
1241 else:
1241 else:
1242 default_redirect_url = h.route_path(
1242 default_redirect_url = h.route_path(
1243 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1243 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1244
1244
1245 # If there's no commit, redirect to repo summary
1245 # If there's no commit, redirect to repo summary
1246 if type(c.commit) is EmptyCommit:
1246 if type(c.commit) is EmptyCommit:
1247 redirect_url = h.route_path(
1247 redirect_url = h.route_path(
1248 'repo_summary', repo_name=self.db_repo_name)
1248 'repo_summary', repo_name=self.db_repo_name)
1249 else:
1249 else:
1250 redirect_url = default_redirect_url
1250 redirect_url = default_redirect_url
1251
1251
1252 if not filename:
1252 if not filename:
1253 h.flash(_('No filename'), category='warning')
1253 h.flash(_('No filename'), category='warning')
1254 raise HTTPFound(redirect_url)
1254 raise HTTPFound(redirect_url)
1255
1255
1256 # extract the location from filename,
1256 # extract the location from filename,
1257 # allows using foo/bar.txt syntax to create subdirectories
1257 # allows using foo/bar.txt syntax to create subdirectories
1258 subdir_loc = filename.rsplit('/', 1)
1258 subdir_loc = filename.rsplit('/', 1)
1259 if len(subdir_loc) == 2:
1259 if len(subdir_loc) == 2:
1260 location = os.path.join(location, subdir_loc[0])
1260 location = os.path.join(location, subdir_loc[0])
1261
1261
1262 # strip all crap out of file, just leave the basename
1262 # strip all crap out of file, just leave the basename
1263 filename = os.path.basename(filename)
1263 filename = os.path.basename(filename)
1264 node_path = os.path.join(location, filename)
1264 node_path = os.path.join(location, filename)
1265 author = self._rhodecode_db_user.full_contact
1265 author = self._rhodecode_db_user.full_contact
1266
1266
1267 try:
1267 try:
1268 nodes = {
1268 nodes = {
1269 node_path: {
1269 node_path: {
1270 'content': content
1270 'content': content
1271 }
1271 }
1272 }
1272 }
1273 ScmModel().create_nodes(
1273 ScmModel().create_nodes(
1274 user=self._rhodecode_db_user.user_id,
1274 user=self._rhodecode_db_user.user_id,
1275 repo=self.db_repo,
1275 repo=self.db_repo,
1276 message=message,
1276 message=message,
1277 nodes=nodes,
1277 nodes=nodes,
1278 parent_commit=c.commit,
1278 parent_commit=c.commit,
1279 author=author,
1279 author=author,
1280 )
1280 )
1281
1281
1282 h.flash(
1282 h.flash(
1283 _('Successfully committed new file `{}`').format(
1283 _('Successfully committed new file `{}`').format(
1284 h.escape(node_path)), category='success')
1284 h.escape(node_path)), category='success')
1285 except NonRelativePathError:
1285 except NonRelativePathError:
1286 log.exception('Non Relative path found')
1286 log.exception('Non Relative path found')
1287 h.flash(_(
1287 h.flash(_(
1288 'The location specified must be a relative path and must not '
1288 'The location specified must be a relative path and must not '
1289 'contain .. in the path'), category='warning')
1289 'contain .. in the path'), category='warning')
1290 raise HTTPFound(default_redirect_url)
1290 raise HTTPFound(default_redirect_url)
1291 except (NodeError, NodeAlreadyExistsError) as e:
1291 except (NodeError, NodeAlreadyExistsError) as e:
1292 h.flash(_(h.escape(e)), category='error')
1292 h.flash(_(h.escape(e)), category='error')
1293 except Exception:
1293 except Exception:
1294 log.exception('Error occurred during commit')
1294 log.exception('Error occurred during commit')
1295 h.flash(_('Error occurred during commit'), category='error')
1295 h.flash(_('Error occurred during commit'), category='error')
1296
1296
1297 raise HTTPFound(default_redirect_url)
1297 raise HTTPFound(default_redirect_url)
@@ -1,379 +1,379 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 string
22 import string
23 import rhodecode
23 import rhodecode
24
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from beaker.cache import cache_region
26 from beaker.cache import cache_region
27
27
28 from rhodecode.controllers import utils
28 from rhodecode.controllers import utils
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
30 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
31 from rhodecode.lib import helpers as h, rc_cache
31 from rhodecode.lib import helpers as h, rc_cache
32 from rhodecode.lib.utils2 import safe_str, safe_int
32 from rhodecode.lib.utils2 import safe_str, safe_int
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
34 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
36 from rhodecode.lib.vcs.backends.base import EmptyCommit
37 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
38 CommitError, EmptyRepositoryError, CommitDoesNotExistError)
39 from rhodecode.model.db import Statistics, CacheKey, User
39 from rhodecode.model.db import Statistics, CacheKey, User
40 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.repo import ReadmeFinder
41 from rhodecode.model.repo import ReadmeFinder
42 from rhodecode.model.scm import ScmModel
42 from rhodecode.model.scm import ScmModel
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class RepoSummaryView(RepoAppView):
47 class RepoSummaryView(RepoAppView):
48
48
49 def load_default_context(self):
49 def load_default_context(self):
50 c = self._get_local_tmpl_context(include_app_defaults=True)
50 c = self._get_local_tmpl_context(include_app_defaults=True)
51 c.rhodecode_repo = None
51 c.rhodecode_repo = None
52 if not c.repository_requirements_missing:
52 if not c.repository_requirements_missing:
53 c.rhodecode_repo = self.rhodecode_vcs_repo
53 c.rhodecode_repo = self.rhodecode_vcs_repo
54 return c
54 return c
55
55
56 def _get_readme_data(self, db_repo, default_renderer):
56 def _get_readme_data(self, db_repo, default_renderer):
57 repo_name = db_repo.repo_name
57 repo_name = db_repo.repo_name
58 log.debug('Looking for README file')
58 log.debug('Looking for README file')
59
59
60 @cache_region('long_term')
60 @cache_region('long_term')
61 def _generate_readme(cache_key):
61 def _generate_readme(cache_key):
62 readme_data = None
62 readme_data = None
63 readme_node = None
63 readme_node = None
64 readme_filename = None
64 readme_filename = None
65 commit = self._get_landing_commit_or_none(db_repo)
65 commit = self._get_landing_commit_or_none(db_repo)
66 if commit:
66 if commit:
67 log.debug("Searching for a README file.")
67 log.debug("Searching for a README file.")
68 readme_node = ReadmeFinder(default_renderer).search(commit)
68 readme_node = ReadmeFinder(default_renderer).search(commit)
69 if readme_node:
69 if readme_node:
70 relative_urls = {
70 relative_urls = {
71 'raw': h.route_path(
71 'raw': h.route_path(
72 'repo_file_raw', repo_name=repo_name,
72 'repo_file_raw', repo_name=repo_name,
73 commit_id=commit.raw_id, f_path=readme_node.path),
73 commit_id=commit.raw_id, f_path=readme_node.path),
74 'standard': h.route_path(
74 'standard': h.route_path(
75 'repo_files', repo_name=repo_name,
75 'repo_files', repo_name=repo_name,
76 commit_id=commit.raw_id, f_path=readme_node.path),
76 commit_id=commit.raw_id, f_path=readme_node.path),
77 }
77 }
78 readme_data = self._render_readme_or_none(
78 readme_data = self._render_readme_or_none(
79 commit, readme_node, relative_urls)
79 commit, readme_node, relative_urls)
80 readme_filename = readme_node.path
80 readme_filename = readme_node.path
81 return readme_data, readme_filename
81 return readme_data, readme_filename
82
82
83 invalidator_context = CacheKey.repo_context_cache(
83 invalidator_context = CacheKey.repo_context_cache(
84 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
84 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
85
85
86 with invalidator_context as context:
86 with invalidator_context as context:
87 context.invalidate()
87 context.invalidate()
88 computed = context.compute()
88 computed = context.compute()
89
89
90 return computed
90 return computed
91
91
92 def _get_landing_commit_or_none(self, db_repo):
92 def _get_landing_commit_or_none(self, db_repo):
93 log.debug("Getting the landing commit.")
93 log.debug("Getting the landing commit.")
94 try:
94 try:
95 commit = db_repo.get_landing_commit()
95 commit = db_repo.get_landing_commit()
96 if not isinstance(commit, EmptyCommit):
96 if not isinstance(commit, EmptyCommit):
97 return commit
97 return commit
98 else:
98 else:
99 log.debug("Repository is empty, no README to render.")
99 log.debug("Repository is empty, no README to render.")
100 except CommitError:
100 except CommitError:
101 log.exception(
101 log.exception(
102 "Problem getting commit when trying to render the README.")
102 "Problem getting commit when trying to render the README.")
103
103
104 def _render_readme_or_none(self, commit, readme_node, relative_urls):
104 def _render_readme_or_none(self, commit, readme_node, relative_urls):
105 log.debug(
105 log.debug(
106 'Found README file `%s` rendering...', readme_node.path)
106 'Found README file `%s` rendering...', readme_node.path)
107 renderer = MarkupRenderer()
107 renderer = MarkupRenderer()
108 try:
108 try:
109 html_source = renderer.render(
109 html_source = renderer.render(
110 readme_node.content, filename=readme_node.path)
110 readme_node.content, filename=readme_node.path)
111 if relative_urls:
111 if relative_urls:
112 return relative_links(html_source, relative_urls)
112 return relative_links(html_source, relative_urls)
113 return html_source
113 return html_source
114 except Exception:
114 except Exception:
115 log.exception(
115 log.exception(
116 "Exception while trying to render the README")
116 "Exception while trying to render the README")
117
117
118 def _load_commits_context(self, c):
118 def _load_commits_context(self, c):
119 p = safe_int(self.request.GET.get('page'), 1)
119 p = safe_int(self.request.GET.get('page'), 1)
120 size = safe_int(self.request.GET.get('size'), 10)
120 size = safe_int(self.request.GET.get('size'), 10)
121
121
122 def url_generator(**kw):
122 def url_generator(**kw):
123 query_params = {
123 query_params = {
124 'size': size
124 'size': size
125 }
125 }
126 query_params.update(kw)
126 query_params.update(kw)
127 return h.route_path(
127 return h.route_path(
128 'repo_summary_commits',
128 'repo_summary_commits',
129 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
129 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
130
130
131 pre_load = ['author', 'branch', 'date', 'message']
131 pre_load = ['author', 'branch', 'date', 'message']
132 try:
132 try:
133 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
133 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
134 except EmptyRepositoryError:
134 except EmptyRepositoryError:
135 collection = self.rhodecode_vcs_repo
135 collection = self.rhodecode_vcs_repo
136
136
137 c.repo_commits = h.RepoPage(
137 c.repo_commits = h.RepoPage(
138 collection, page=p, items_per_page=size, url=url_generator)
138 collection, page=p, items_per_page=size, url=url_generator)
139 page_ids = [x.raw_id for x in c.repo_commits]
139 page_ids = [x.raw_id for x in c.repo_commits]
140 c.comments = self.db_repo.get_comments(page_ids)
140 c.comments = self.db_repo.get_comments(page_ids)
141 c.statuses = self.db_repo.statuses(page_ids)
141 c.statuses = self.db_repo.statuses(page_ids)
142
142
143 @LoginRequired()
143 @LoginRequired()
144 @HasRepoPermissionAnyDecorator(
144 @HasRepoPermissionAnyDecorator(
145 'repository.read', 'repository.write', 'repository.admin')
145 'repository.read', 'repository.write', 'repository.admin')
146 @view_config(
146 @view_config(
147 route_name='repo_summary_commits', request_method='GET',
147 route_name='repo_summary_commits', request_method='GET',
148 renderer='rhodecode:templates/summary/summary_commits.mako')
148 renderer='rhodecode:templates/summary/summary_commits.mako')
149 def summary_commits(self):
149 def summary_commits(self):
150 c = self.load_default_context()
150 c = self.load_default_context()
151 self._load_commits_context(c)
151 self._load_commits_context(c)
152 return self._get_template_context(c)
152 return self._get_template_context(c)
153
153
154 @LoginRequired()
154 @LoginRequired()
155 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
156 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
157 @view_config(
157 @view_config(
158 route_name='repo_summary', request_method='GET',
158 route_name='repo_summary', request_method='GET',
159 renderer='rhodecode:templates/summary/summary.mako')
159 renderer='rhodecode:templates/summary/summary.mako')
160 @view_config(
160 @view_config(
161 route_name='repo_summary_slash', request_method='GET',
161 route_name='repo_summary_slash', request_method='GET',
162 renderer='rhodecode:templates/summary/summary.mako')
162 renderer='rhodecode:templates/summary/summary.mako')
163 @view_config(
163 @view_config(
164 route_name='repo_summary_explicit', request_method='GET',
164 route_name='repo_summary_explicit', request_method='GET',
165 renderer='rhodecode:templates/summary/summary.mako')
165 renderer='rhodecode:templates/summary/summary.mako')
166 def summary(self):
166 def summary(self):
167 c = self.load_default_context()
167 c = self.load_default_context()
168
168
169 # Prepare the clone URL
169 # Prepare the clone URL
170 username = ''
170 username = ''
171 if self._rhodecode_user.username != User.DEFAULT_USER:
171 if self._rhodecode_user.username != User.DEFAULT_USER:
172 username = safe_str(self._rhodecode_user.username)
172 username = safe_str(self._rhodecode_user.username)
173
173
174 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
174 _def_clone_uri = _def_clone_uri_id = c.clone_uri_tmpl
175 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
175 _def_clone_uri_ssh = c.clone_uri_ssh_tmpl
176
176
177 if '{repo}' in _def_clone_uri:
177 if '{repo}' in _def_clone_uri:
178 _def_clone_uri_id = _def_clone_uri.replace(
178 _def_clone_uri_id = _def_clone_uri.replace(
179 '{repo}', '_{repoid}')
179 '{repo}', '_{repoid}')
180 elif '{repoid}' in _def_clone_uri:
180 elif '{repoid}' in _def_clone_uri:
181 _def_clone_uri_id = _def_clone_uri.replace(
181 _def_clone_uri_id = _def_clone_uri.replace(
182 '_{repoid}', '{repo}')
182 '_{repoid}', '{repo}')
183
183
184 c.clone_repo_url = self.db_repo.clone_url(
184 c.clone_repo_url = self.db_repo.clone_url(
185 user=username, uri_tmpl=_def_clone_uri)
185 user=username, uri_tmpl=_def_clone_uri)
186 c.clone_repo_url_id = self.db_repo.clone_url(
186 c.clone_repo_url_id = self.db_repo.clone_url(
187 user=username, uri_tmpl=_def_clone_uri_id)
187 user=username, uri_tmpl=_def_clone_uri_id)
188 c.clone_repo_url_ssh = self.db_repo.clone_url(
188 c.clone_repo_url_ssh = self.db_repo.clone_url(
189 uri_tmpl=_def_clone_uri_ssh, ssh=True)
189 uri_tmpl=_def_clone_uri_ssh, ssh=True)
190
190
191 # If enabled, get statistics data
191 # If enabled, get statistics data
192
192
193 c.show_stats = bool(self.db_repo.enable_statistics)
193 c.show_stats = bool(self.db_repo.enable_statistics)
194
194
195 stats = Session().query(Statistics) \
195 stats = Session().query(Statistics) \
196 .filter(Statistics.repository == self.db_repo) \
196 .filter(Statistics.repository == self.db_repo) \
197 .scalar()
197 .scalar()
198
198
199 c.stats_percentage = 0
199 c.stats_percentage = 0
200
200
201 if stats and stats.languages:
201 if stats and stats.languages:
202 c.no_data = False is self.db_repo.enable_statistics
202 c.no_data = False is self.db_repo.enable_statistics
203 lang_stats_d = json.loads(stats.languages)
203 lang_stats_d = json.loads(stats.languages)
204
204
205 # Sort first by decreasing count and second by the file extension,
205 # Sort first by decreasing count and second by the file extension,
206 # so we have a consistent output.
206 # so we have a consistent output.
207 lang_stats_items = sorted(lang_stats_d.iteritems(),
207 lang_stats_items = sorted(lang_stats_d.iteritems(),
208 key=lambda k: (-k[1], k[0]))[:10]
208 key=lambda k: (-k[1], k[0]))[:10]
209 lang_stats = [(x, {"count": y,
209 lang_stats = [(x, {"count": y,
210 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
210 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
211 for x, y in lang_stats_items]
211 for x, y in lang_stats_items]
212
212
213 c.trending_languages = json.dumps(lang_stats)
213 c.trending_languages = json.dumps(lang_stats)
214 else:
214 else:
215 c.no_data = True
215 c.no_data = True
216 c.trending_languages = json.dumps({})
216 c.trending_languages = json.dumps({})
217
217
218 scm_model = ScmModel()
218 scm_model = ScmModel()
219 c.enable_downloads = self.db_repo.enable_downloads
219 c.enable_downloads = self.db_repo.enable_downloads
220 c.repository_followers = scm_model.get_followers(self.db_repo)
220 c.repository_followers = scm_model.get_followers(self.db_repo)
221 c.repository_forks = scm_model.get_forks(self.db_repo)
221 c.repository_forks = scm_model.get_forks(self.db_repo)
222 c.repository_is_user_following = scm_model.is_following_repo(
222 c.repository_is_user_following = scm_model.is_following_repo(
223 self.db_repo_name, self._rhodecode_user.user_id)
223 self.db_repo_name, self._rhodecode_user.user_id)
224
224
225 # first interaction with the VCS instance after here...
225 # first interaction with the VCS instance after here...
226 if c.repository_requirements_missing:
226 if c.repository_requirements_missing:
227 self.request.override_renderer = \
227 self.request.override_renderer = \
228 'rhodecode:templates/summary/missing_requirements.mako'
228 'rhodecode:templates/summary/missing_requirements.mako'
229 return self._get_template_context(c)
229 return self._get_template_context(c)
230
230
231 c.readme_data, c.readme_file = \
231 c.readme_data, c.readme_file = \
232 self._get_readme_data(self.db_repo, c.visual.default_renderer)
232 self._get_readme_data(self.db_repo, c.visual.default_renderer)
233
233
234 # loads the summary commits template context
234 # loads the summary commits template context
235 self._load_commits_context(c)
235 self._load_commits_context(c)
236
236
237 return self._get_template_context(c)
237 return self._get_template_context(c)
238
238
239 def get_request_commit_id(self):
239 def get_request_commit_id(self):
240 return self.request.matchdict['commit_id']
240 return self.request.matchdict['commit_id']
241
241
242 @LoginRequired()
242 @LoginRequired()
243 @HasRepoPermissionAnyDecorator(
243 @HasRepoPermissionAnyDecorator(
244 'repository.read', 'repository.write', 'repository.admin')
244 'repository.read', 'repository.write', 'repository.admin')
245 @view_config(
245 @view_config(
246 route_name='repo_stats', request_method='GET',
246 route_name='repo_stats', request_method='GET',
247 renderer='json_ext')
247 renderer='json_ext')
248 def repo_stats(self):
248 def repo_stats(self):
249 commit_id = self.get_request_commit_id()
249 commit_id = self.get_request_commit_id()
250 show_stats = bool(self.db_repo.enable_statistics)
250 show_stats = bool(self.db_repo.enable_statistics)
251 repo_id = self.db_repo.repo_id
251 repo_id = self.db_repo.repo_id
252
252
253 cache_seconds = safe_int(
253 cache_seconds = safe_int(
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 cache_on = cache_seconds > 0
255 cache_on = cache_seconds > 0
256 log.debug(
256 log.debug(
257 'Computing REPO TREE for repo_id %s commit_id `%s` '
257 'Computing REPO TREE for repo_id %s commit_id `%s` '
258 'with caching: %s[TTL: %ss]' % (
258 'with caching: %s[TTL: %ss]' % (
259 repo_id, commit_id, cache_on, cache_seconds or 0))
259 repo_id, commit_id, cache_on, cache_seconds or 0))
260
260
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263
263
264 @region.cache_on_arguments(namespace=cache_namespace_uid,
264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
265 should_cache_fn=lambda v: cache_on)
265 condition=cache_on)
266 def compute_stats(repo_id, commit_id, show_stats):
266 def compute_stats(repo_id, commit_id, show_stats):
267 code_stats = {}
267 code_stats = {}
268 size = 0
268 size = 0
269 try:
269 try:
270 scm_instance = self.db_repo.scm_instance()
270 scm_instance = self.db_repo.scm_instance()
271 commit = scm_instance.get_commit(commit_id)
271 commit = scm_instance.get_commit(commit_id)
272
272
273 for node in commit.get_filenodes_generator():
273 for node in commit.get_filenodes_generator():
274 size += node.size
274 size += node.size
275 if not show_stats:
275 if not show_stats:
276 continue
276 continue
277 ext = string.lower(node.extension)
277 ext = string.lower(node.extension)
278 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
278 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
279 if ext_info:
279 if ext_info:
280 if ext in code_stats:
280 if ext in code_stats:
281 code_stats[ext]['count'] += 1
281 code_stats[ext]['count'] += 1
282 else:
282 else:
283 code_stats[ext] = {"count": 1, "desc": ext_info}
283 code_stats[ext] = {"count": 1, "desc": ext_info}
284 except (EmptyRepositoryError, CommitDoesNotExistError):
284 except (EmptyRepositoryError, CommitDoesNotExistError):
285 pass
285 pass
286 return {'size': h.format_byte_size_binary(size),
286 return {'size': h.format_byte_size_binary(size),
287 'code_stats': code_stats}
287 'code_stats': code_stats}
288
288
289 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
289 stats = compute_stats(self.db_repo.repo_id, commit_id, show_stats)
290 return stats
290 return stats
291
291
292 @LoginRequired()
292 @LoginRequired()
293 @HasRepoPermissionAnyDecorator(
293 @HasRepoPermissionAnyDecorator(
294 'repository.read', 'repository.write', 'repository.admin')
294 'repository.read', 'repository.write', 'repository.admin')
295 @view_config(
295 @view_config(
296 route_name='repo_refs_data', request_method='GET',
296 route_name='repo_refs_data', request_method='GET',
297 renderer='json_ext')
297 renderer='json_ext')
298 def repo_refs_data(self):
298 def repo_refs_data(self):
299 _ = self.request.translate
299 _ = self.request.translate
300 self.load_default_context()
300 self.load_default_context()
301
301
302 repo = self.rhodecode_vcs_repo
302 repo = self.rhodecode_vcs_repo
303 refs_to_create = [
303 refs_to_create = [
304 (_("Branch"), repo.branches, 'branch'),
304 (_("Branch"), repo.branches, 'branch'),
305 (_("Tag"), repo.tags, 'tag'),
305 (_("Tag"), repo.tags, 'tag'),
306 (_("Bookmark"), repo.bookmarks, 'book'),
306 (_("Bookmark"), repo.bookmarks, 'book'),
307 ]
307 ]
308 res = self._create_reference_data(
308 res = self._create_reference_data(
309 repo, self.db_repo_name, refs_to_create)
309 repo, self.db_repo_name, refs_to_create)
310 data = {
310 data = {
311 'more': False,
311 'more': False,
312 'results': res
312 'results': res
313 }
313 }
314 return data
314 return data
315
315
316 @LoginRequired()
316 @LoginRequired()
317 @HasRepoPermissionAnyDecorator(
317 @HasRepoPermissionAnyDecorator(
318 'repository.read', 'repository.write', 'repository.admin')
318 'repository.read', 'repository.write', 'repository.admin')
319 @view_config(
319 @view_config(
320 route_name='repo_refs_changelog_data', request_method='GET',
320 route_name='repo_refs_changelog_data', request_method='GET',
321 renderer='json_ext')
321 renderer='json_ext')
322 def repo_refs_changelog_data(self):
322 def repo_refs_changelog_data(self):
323 _ = self.request.translate
323 _ = self.request.translate
324 self.load_default_context()
324 self.load_default_context()
325
325
326 repo = self.rhodecode_vcs_repo
326 repo = self.rhodecode_vcs_repo
327
327
328 refs_to_create = [
328 refs_to_create = [
329 (_("Branches"), repo.branches, 'branch'),
329 (_("Branches"), repo.branches, 'branch'),
330 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
330 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
331 # TODO: enable when vcs can handle bookmarks filters
331 # TODO: enable when vcs can handle bookmarks filters
332 # (_("Bookmarks"), repo.bookmarks, "book"),
332 # (_("Bookmarks"), repo.bookmarks, "book"),
333 ]
333 ]
334 res = self._create_reference_data(
334 res = self._create_reference_data(
335 repo, self.db_repo_name, refs_to_create)
335 repo, self.db_repo_name, refs_to_create)
336 data = {
336 data = {
337 'more': False,
337 'more': False,
338 'results': res
338 'results': res
339 }
339 }
340 return data
340 return data
341
341
342 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
342 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
343 format_ref_id = utils.get_format_ref_id(repo)
343 format_ref_id = utils.get_format_ref_id(repo)
344
344
345 result = []
345 result = []
346 for title, refs, ref_type in refs_to_create:
346 for title, refs, ref_type in refs_to_create:
347 if refs:
347 if refs:
348 result.append({
348 result.append({
349 'text': title,
349 'text': title,
350 'children': self._create_reference_items(
350 'children': self._create_reference_items(
351 repo, full_repo_name, refs, ref_type,
351 repo, full_repo_name, refs, ref_type,
352 format_ref_id),
352 format_ref_id),
353 })
353 })
354 return result
354 return result
355
355
356 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
356 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
357 format_ref_id):
357 format_ref_id):
358 result = []
358 result = []
359 is_svn = h.is_svn(repo)
359 is_svn = h.is_svn(repo)
360 for ref_name, raw_id in refs.iteritems():
360 for ref_name, raw_id in refs.iteritems():
361 files_url = self._create_files_url(
361 files_url = self._create_files_url(
362 repo, full_repo_name, ref_name, raw_id, is_svn)
362 repo, full_repo_name, ref_name, raw_id, is_svn)
363 result.append({
363 result.append({
364 'text': ref_name,
364 'text': ref_name,
365 'id': format_ref_id(ref_name, raw_id),
365 'id': format_ref_id(ref_name, raw_id),
366 'raw_id': raw_id,
366 'raw_id': raw_id,
367 'type': ref_type,
367 'type': ref_type,
368 'files_url': files_url,
368 'files_url': files_url,
369 })
369 })
370 return result
370 return result
371
371
372 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
372 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
373 use_commit_id = '/' in ref_name or is_svn
373 use_commit_id = '/' in ref_name or is_svn
374 return h.route_path(
374 return h.route_path(
375 'repo_files',
375 'repo_files',
376 repo_name=full_repo_name,
376 repo_name=full_repo_name,
377 f_path=ref_name if is_svn else '',
377 f_path=ref_name if is_svn else '',
378 commit_id=raw_id if use_commit_id else ref_name,
378 commit_id=raw_id if use_commit_id else ref_name,
379 _query=dict(at=ref_name))
379 _query=dict(at=ref_name))
@@ -1,759 +1,759 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 Authentication modules
22 Authentication modules
23 """
23 """
24 import socket
24 import socket
25 import string
25 import string
26 import colander
26 import colander
27 import copy
27 import copy
28 import logging
28 import logging
29 import time
29 import time
30 import traceback
30 import traceback
31 import warnings
31 import warnings
32 import functools
32 import functools
33
33
34 from pyramid.threadlocal import get_current_registry
34 from pyramid.threadlocal import get_current_registry
35
35
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
36 from rhodecode.authentication.interface import IAuthnPluginRegistry
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
38 from rhodecode.lib import caches, rc_cache
38 from rhodecode.lib import caches, rc_cache
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
39 from rhodecode.lib.auth import PasswordGenerator, _RhodeCodeCryptoBCrypt
40 from rhodecode.lib.utils2 import safe_int, safe_str
40 from rhodecode.lib.utils2 import safe_int, safe_str
41 from rhodecode.lib.exceptions import LdapConnectionError
41 from rhodecode.lib.exceptions import LdapConnectionError
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.model.meta import Session
43 from rhodecode.model.meta import Session
44 from rhodecode.model.settings import SettingsModel
44 from rhodecode.model.settings import SettingsModel
45 from rhodecode.model.user import UserModel
45 from rhodecode.model.user import UserModel
46 from rhodecode.model.user_group import UserGroupModel
46 from rhodecode.model.user_group import UserGroupModel
47
47
48
48
49 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
50
50
51 # auth types that authenticate() function can receive
51 # auth types that authenticate() function can receive
52 VCS_TYPE = 'vcs'
52 VCS_TYPE = 'vcs'
53 HTTP_TYPE = 'http'
53 HTTP_TYPE = 'http'
54
54
55
55
56 class hybrid_property(object):
56 class hybrid_property(object):
57 """
57 """
58 a property decorator that works both for instance and class
58 a property decorator that works both for instance and class
59 """
59 """
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
60 def __init__(self, fget, fset=None, fdel=None, expr=None):
61 self.fget = fget
61 self.fget = fget
62 self.fset = fset
62 self.fset = fset
63 self.fdel = fdel
63 self.fdel = fdel
64 self.expr = expr or fget
64 self.expr = expr or fget
65 functools.update_wrapper(self, fget)
65 functools.update_wrapper(self, fget)
66
66
67 def __get__(self, instance, owner):
67 def __get__(self, instance, owner):
68 if instance is None:
68 if instance is None:
69 return self.expr(owner)
69 return self.expr(owner)
70 else:
70 else:
71 return self.fget(instance)
71 return self.fget(instance)
72
72
73 def __set__(self, instance, value):
73 def __set__(self, instance, value):
74 self.fset(instance, value)
74 self.fset(instance, value)
75
75
76 def __delete__(self, instance):
76 def __delete__(self, instance):
77 self.fdel(instance)
77 self.fdel(instance)
78
78
79
79
80 class LazyFormencode(object):
80 class LazyFormencode(object):
81 def __init__(self, formencode_obj, *args, **kwargs):
81 def __init__(self, formencode_obj, *args, **kwargs):
82 self.formencode_obj = formencode_obj
82 self.formencode_obj = formencode_obj
83 self.args = args
83 self.args = args
84 self.kwargs = kwargs
84 self.kwargs = kwargs
85
85
86 def __call__(self, *args, **kwargs):
86 def __call__(self, *args, **kwargs):
87 from inspect import isfunction
87 from inspect import isfunction
88 formencode_obj = self.formencode_obj
88 formencode_obj = self.formencode_obj
89 if isfunction(formencode_obj):
89 if isfunction(formencode_obj):
90 # case we wrap validators into functions
90 # case we wrap validators into functions
91 formencode_obj = self.formencode_obj(*args, **kwargs)
91 formencode_obj = self.formencode_obj(*args, **kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
92 return formencode_obj(*self.args, **self.kwargs)
93
93
94
94
95 class RhodeCodeAuthPluginBase(object):
95 class RhodeCodeAuthPluginBase(object):
96 # cache the authentication request for N amount of seconds. Some kind
96 # cache the authentication request for N amount of seconds. Some kind
97 # of authentication methods are very heavy and it's very efficient to cache
97 # of authentication methods are very heavy and it's very efficient to cache
98 # the result of a call. If it's set to None (default) cache is off
98 # the result of a call. If it's set to None (default) cache is off
99 AUTH_CACHE_TTL = None
99 AUTH_CACHE_TTL = None
100 AUTH_CACHE = {}
100 AUTH_CACHE = {}
101
101
102 auth_func_attrs = {
102 auth_func_attrs = {
103 "username": "unique username",
103 "username": "unique username",
104 "firstname": "first name",
104 "firstname": "first name",
105 "lastname": "last name",
105 "lastname": "last name",
106 "email": "email address",
106 "email": "email address",
107 "groups": '["list", "of", "groups"]',
107 "groups": '["list", "of", "groups"]',
108 "user_group_sync":
108 "user_group_sync":
109 'True|False defines if returned user groups should be synced',
109 'True|False defines if returned user groups should be synced',
110 "extern_name": "name in external source of record",
110 "extern_name": "name in external source of record",
111 "extern_type": "type of external source of record",
111 "extern_type": "type of external source of record",
112 "admin": 'True|False defines if user should be RhodeCode super admin',
112 "admin": 'True|False defines if user should be RhodeCode super admin',
113 "active":
113 "active":
114 'True|False defines active state of user internally for RhodeCode',
114 'True|False defines active state of user internally for RhodeCode',
115 "active_from_extern":
115 "active_from_extern":
116 "True|False\None, active state from the external auth, "
116 "True|False\None, active state from the external auth, "
117 "None means use definition from RhodeCode extern_type active value"
117 "None means use definition from RhodeCode extern_type active value"
118
118
119 }
119 }
120 # set on authenticate() method and via set_auth_type func.
120 # set on authenticate() method and via set_auth_type func.
121 auth_type = None
121 auth_type = None
122
122
123 # set on authenticate() method and via set_calling_scope_repo, this is a
123 # set on authenticate() method and via set_calling_scope_repo, this is a
124 # calling scope repository when doing authentication most likely on VCS
124 # calling scope repository when doing authentication most likely on VCS
125 # operations
125 # operations
126 acl_repo_name = None
126 acl_repo_name = None
127
127
128 # List of setting names to store encrypted. Plugins may override this list
128 # List of setting names to store encrypted. Plugins may override this list
129 # to store settings encrypted.
129 # to store settings encrypted.
130 _settings_encrypted = []
130 _settings_encrypted = []
131
131
132 # Mapping of python to DB settings model types. Plugins may override or
132 # Mapping of python to DB settings model types. Plugins may override or
133 # extend this mapping.
133 # extend this mapping.
134 _settings_type_map = {
134 _settings_type_map = {
135 colander.String: 'unicode',
135 colander.String: 'unicode',
136 colander.Integer: 'int',
136 colander.Integer: 'int',
137 colander.Boolean: 'bool',
137 colander.Boolean: 'bool',
138 colander.List: 'list',
138 colander.List: 'list',
139 }
139 }
140
140
141 # list of keys in settings that are unsafe to be logged, should be passwords
141 # list of keys in settings that are unsafe to be logged, should be passwords
142 # or other crucial credentials
142 # or other crucial credentials
143 _settings_unsafe_keys = []
143 _settings_unsafe_keys = []
144
144
145 def __init__(self, plugin_id):
145 def __init__(self, plugin_id):
146 self._plugin_id = plugin_id
146 self._plugin_id = plugin_id
147
147
148 def __str__(self):
148 def __str__(self):
149 return self.get_id()
149 return self.get_id()
150
150
151 def _get_setting_full_name(self, name):
151 def _get_setting_full_name(self, name):
152 """
152 """
153 Return the full setting name used for storing values in the database.
153 Return the full setting name used for storing values in the database.
154 """
154 """
155 # TODO: johbo: Using the name here is problematic. It would be good to
155 # TODO: johbo: Using the name here is problematic. It would be good to
156 # introduce either new models in the database to hold Plugin and
156 # introduce either new models in the database to hold Plugin and
157 # PluginSetting or to use the plugin id here.
157 # PluginSetting or to use the plugin id here.
158 return 'auth_{}_{}'.format(self.name, name)
158 return 'auth_{}_{}'.format(self.name, name)
159
159
160 def _get_setting_type(self, name):
160 def _get_setting_type(self, name):
161 """
161 """
162 Return the type of a setting. This type is defined by the SettingsModel
162 Return the type of a setting. This type is defined by the SettingsModel
163 and determines how the setting is stored in DB. Optionally the suffix
163 and determines how the setting is stored in DB. Optionally the suffix
164 `.encrypted` is appended to instruct SettingsModel to store it
164 `.encrypted` is appended to instruct SettingsModel to store it
165 encrypted.
165 encrypted.
166 """
166 """
167 schema_node = self.get_settings_schema().get(name)
167 schema_node = self.get_settings_schema().get(name)
168 db_type = self._settings_type_map.get(
168 db_type = self._settings_type_map.get(
169 type(schema_node.typ), 'unicode')
169 type(schema_node.typ), 'unicode')
170 if name in self._settings_encrypted:
170 if name in self._settings_encrypted:
171 db_type = '{}.encrypted'.format(db_type)
171 db_type = '{}.encrypted'.format(db_type)
172 return db_type
172 return db_type
173
173
174 def is_enabled(self):
174 def is_enabled(self):
175 """
175 """
176 Returns true if this plugin is enabled. An enabled plugin can be
176 Returns true if this plugin is enabled. An enabled plugin can be
177 configured in the admin interface but it is not consulted during
177 configured in the admin interface but it is not consulted during
178 authentication.
178 authentication.
179 """
179 """
180 auth_plugins = SettingsModel().get_auth_plugins()
180 auth_plugins = SettingsModel().get_auth_plugins()
181 return self.get_id() in auth_plugins
181 return self.get_id() in auth_plugins
182
182
183 def is_active(self, plugin_cached_settings=None):
183 def is_active(self, plugin_cached_settings=None):
184 """
184 """
185 Returns true if the plugin is activated. An activated plugin is
185 Returns true if the plugin is activated. An activated plugin is
186 consulted during authentication, assumed it is also enabled.
186 consulted during authentication, assumed it is also enabled.
187 """
187 """
188 return self.get_setting_by_name(
188 return self.get_setting_by_name(
189 'enabled', plugin_cached_settings=plugin_cached_settings)
189 'enabled', plugin_cached_settings=plugin_cached_settings)
190
190
191 def get_id(self):
191 def get_id(self):
192 """
192 """
193 Returns the plugin id.
193 Returns the plugin id.
194 """
194 """
195 return self._plugin_id
195 return self._plugin_id
196
196
197 def get_display_name(self):
197 def get_display_name(self):
198 """
198 """
199 Returns a translation string for displaying purposes.
199 Returns a translation string for displaying purposes.
200 """
200 """
201 raise NotImplementedError('Not implemented in base class')
201 raise NotImplementedError('Not implemented in base class')
202
202
203 def get_settings_schema(self):
203 def get_settings_schema(self):
204 """
204 """
205 Returns a colander schema, representing the plugin settings.
205 Returns a colander schema, representing the plugin settings.
206 """
206 """
207 return AuthnPluginSettingsSchemaBase()
207 return AuthnPluginSettingsSchemaBase()
208
208
209 def get_settings(self):
209 def get_settings(self):
210 """
210 """
211 Returns the plugin settings as dictionary.
211 Returns the plugin settings as dictionary.
212 """
212 """
213 settings = {}
213 settings = {}
214 raw_settings = SettingsModel().get_all_settings()
214 raw_settings = SettingsModel().get_all_settings()
215 for node in self.get_settings_schema():
215 for node in self.get_settings_schema():
216 settings[node.name] = self.get_setting_by_name(
216 settings[node.name] = self.get_setting_by_name(
217 node.name, plugin_cached_settings=raw_settings)
217 node.name, plugin_cached_settings=raw_settings)
218 return settings
218 return settings
219
219
220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
220 def get_setting_by_name(self, name, default=None, plugin_cached_settings=None):
221 """
221 """
222 Returns a plugin setting by name.
222 Returns a plugin setting by name.
223 """
223 """
224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
224 full_name = 'rhodecode_{}'.format(self._get_setting_full_name(name))
225 if plugin_cached_settings:
225 if plugin_cached_settings:
226 plugin_settings = plugin_cached_settings
226 plugin_settings = plugin_cached_settings
227 else:
227 else:
228 plugin_settings = SettingsModel().get_all_settings()
228 plugin_settings = SettingsModel().get_all_settings()
229
229
230 if full_name in plugin_settings:
230 if full_name in plugin_settings:
231 return plugin_settings[full_name]
231 return plugin_settings[full_name]
232 else:
232 else:
233 return default
233 return default
234
234
235 def create_or_update_setting(self, name, value):
235 def create_or_update_setting(self, name, value):
236 """
236 """
237 Create or update a setting for this plugin in the persistent storage.
237 Create or update a setting for this plugin in the persistent storage.
238 """
238 """
239 full_name = self._get_setting_full_name(name)
239 full_name = self._get_setting_full_name(name)
240 type_ = self._get_setting_type(name)
240 type_ = self._get_setting_type(name)
241 db_setting = SettingsModel().create_or_update_setting(
241 db_setting = SettingsModel().create_or_update_setting(
242 full_name, value, type_)
242 full_name, value, type_)
243 return db_setting.app_settings_value
243 return db_setting.app_settings_value
244
244
245 def log_safe_settings(self, settings):
245 def log_safe_settings(self, settings):
246 """
246 """
247 returns a log safe representation of settings, without any secrets
247 returns a log safe representation of settings, without any secrets
248 """
248 """
249 settings_copy = copy.deepcopy(settings)
249 settings_copy = copy.deepcopy(settings)
250 for k in self._settings_unsafe_keys:
250 for k in self._settings_unsafe_keys:
251 if k in settings_copy:
251 if k in settings_copy:
252 del settings_copy[k]
252 del settings_copy[k]
253 return settings_copy
253 return settings_copy
254
254
255 @hybrid_property
255 @hybrid_property
256 def name(self):
256 def name(self):
257 """
257 """
258 Returns the name of this authentication plugin.
258 Returns the name of this authentication plugin.
259
259
260 :returns: string
260 :returns: string
261 """
261 """
262 raise NotImplementedError("Not implemented in base class")
262 raise NotImplementedError("Not implemented in base class")
263
263
264 def get_url_slug(self):
264 def get_url_slug(self):
265 """
265 """
266 Returns a slug which should be used when constructing URLs which refer
266 Returns a slug which should be used when constructing URLs which refer
267 to this plugin. By default it returns the plugin name. If the name is
267 to this plugin. By default it returns the plugin name. If the name is
268 not suitable for using it in an URL the plugin should override this
268 not suitable for using it in an URL the plugin should override this
269 method.
269 method.
270 """
270 """
271 return self.name
271 return self.name
272
272
273 @property
273 @property
274 def is_headers_auth(self):
274 def is_headers_auth(self):
275 """
275 """
276 Returns True if this authentication plugin uses HTTP headers as
276 Returns True if this authentication plugin uses HTTP headers as
277 authentication method.
277 authentication method.
278 """
278 """
279 return False
279 return False
280
280
281 @hybrid_property
281 @hybrid_property
282 def is_container_auth(self):
282 def is_container_auth(self):
283 """
283 """
284 Deprecated method that indicates if this authentication plugin uses
284 Deprecated method that indicates if this authentication plugin uses
285 HTTP headers as authentication method.
285 HTTP headers as authentication method.
286 """
286 """
287 warnings.warn(
287 warnings.warn(
288 'Use is_headers_auth instead.', category=DeprecationWarning)
288 'Use is_headers_auth instead.', category=DeprecationWarning)
289 return self.is_headers_auth
289 return self.is_headers_auth
290
290
291 @hybrid_property
291 @hybrid_property
292 def allows_creating_users(self):
292 def allows_creating_users(self):
293 """
293 """
294 Defines if Plugin allows users to be created on-the-fly when
294 Defines if Plugin allows users to be created on-the-fly when
295 authentication is called. Controls how external plugins should behave
295 authentication is called. Controls how external plugins should behave
296 in terms if they are allowed to create new users, or not. Base plugins
296 in terms if they are allowed to create new users, or not. Base plugins
297 should not be allowed to, but External ones should be !
297 should not be allowed to, but External ones should be !
298
298
299 :return: bool
299 :return: bool
300 """
300 """
301 return False
301 return False
302
302
303 def set_auth_type(self, auth_type):
303 def set_auth_type(self, auth_type):
304 self.auth_type = auth_type
304 self.auth_type = auth_type
305
305
306 def set_calling_scope_repo(self, acl_repo_name):
306 def set_calling_scope_repo(self, acl_repo_name):
307 self.acl_repo_name = acl_repo_name
307 self.acl_repo_name = acl_repo_name
308
308
309 def allows_authentication_from(
309 def allows_authentication_from(
310 self, user, allows_non_existing_user=True,
310 self, user, allows_non_existing_user=True,
311 allowed_auth_plugins=None, allowed_auth_sources=None):
311 allowed_auth_plugins=None, allowed_auth_sources=None):
312 """
312 """
313 Checks if this authentication module should accept a request for
313 Checks if this authentication module should accept a request for
314 the current user.
314 the current user.
315
315
316 :param user: user object fetched using plugin's get_user() method.
316 :param user: user object fetched using plugin's get_user() method.
317 :param allows_non_existing_user: if True, don't allow the
317 :param allows_non_existing_user: if True, don't allow the
318 user to be empty, meaning not existing in our database
318 user to be empty, meaning not existing in our database
319 :param allowed_auth_plugins: if provided, users extern_type will be
319 :param allowed_auth_plugins: if provided, users extern_type will be
320 checked against a list of provided extern types, which are plugin
320 checked against a list of provided extern types, which are plugin
321 auth_names in the end
321 auth_names in the end
322 :param allowed_auth_sources: authentication type allowed,
322 :param allowed_auth_sources: authentication type allowed,
323 `http` or `vcs` default is both.
323 `http` or `vcs` default is both.
324 defines if plugin will accept only http authentication vcs
324 defines if plugin will accept only http authentication vcs
325 authentication(git/hg) or both
325 authentication(git/hg) or both
326 :returns: boolean
326 :returns: boolean
327 """
327 """
328 if not user and not allows_non_existing_user:
328 if not user and not allows_non_existing_user:
329 log.debug('User is empty but plugin does not allow empty users,'
329 log.debug('User is empty but plugin does not allow empty users,'
330 'not allowed to authenticate')
330 'not allowed to authenticate')
331 return False
331 return False
332
332
333 expected_auth_plugins = allowed_auth_plugins or [self.name]
333 expected_auth_plugins = allowed_auth_plugins or [self.name]
334 if user and (user.extern_type and
334 if user and (user.extern_type and
335 user.extern_type not in expected_auth_plugins):
335 user.extern_type not in expected_auth_plugins):
336 log.debug(
336 log.debug(
337 'User `%s` is bound to `%s` auth type. Plugin allows only '
337 'User `%s` is bound to `%s` auth type. Plugin allows only '
338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
338 '%s, skipping', user, user.extern_type, expected_auth_plugins)
339
339
340 return False
340 return False
341
341
342 # by default accept both
342 # by default accept both
343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
343 expected_auth_from = allowed_auth_sources or [HTTP_TYPE, VCS_TYPE]
344 if self.auth_type not in expected_auth_from:
344 if self.auth_type not in expected_auth_from:
345 log.debug('Current auth source is %s but plugin only allows %s',
345 log.debug('Current auth source is %s but plugin only allows %s',
346 self.auth_type, expected_auth_from)
346 self.auth_type, expected_auth_from)
347 return False
347 return False
348
348
349 return True
349 return True
350
350
351 def get_user(self, username=None, **kwargs):
351 def get_user(self, username=None, **kwargs):
352 """
352 """
353 Helper method for user fetching in plugins, by default it's using
353 Helper method for user fetching in plugins, by default it's using
354 simple fetch by username, but this method can be custimized in plugins
354 simple fetch by username, but this method can be custimized in plugins
355 eg. headers auth plugin to fetch user by environ params
355 eg. headers auth plugin to fetch user by environ params
356
356
357 :param username: username if given to fetch from database
357 :param username: username if given to fetch from database
358 :param kwargs: extra arguments needed for user fetching.
358 :param kwargs: extra arguments needed for user fetching.
359 """
359 """
360 user = None
360 user = None
361 log.debug(
361 log.debug(
362 'Trying to fetch user `%s` from RhodeCode database', username)
362 'Trying to fetch user `%s` from RhodeCode database', username)
363 if username:
363 if username:
364 user = User.get_by_username(username)
364 user = User.get_by_username(username)
365 if not user:
365 if not user:
366 log.debug('User not found, fallback to fetch user in '
366 log.debug('User not found, fallback to fetch user in '
367 'case insensitive mode')
367 'case insensitive mode')
368 user = User.get_by_username(username, case_insensitive=True)
368 user = User.get_by_username(username, case_insensitive=True)
369 else:
369 else:
370 log.debug('provided username:`%s` is empty skipping...', username)
370 log.debug('provided username:`%s` is empty skipping...', username)
371 if not user:
371 if not user:
372 log.debug('User `%s` not found in database', username)
372 log.debug('User `%s` not found in database', username)
373 else:
373 else:
374 log.debug('Got DB user:%s', user)
374 log.debug('Got DB user:%s', user)
375 return user
375 return user
376
376
377 def user_activation_state(self):
377 def user_activation_state(self):
378 """
378 """
379 Defines user activation state when creating new users
379 Defines user activation state when creating new users
380
380
381 :returns: boolean
381 :returns: boolean
382 """
382 """
383 raise NotImplementedError("Not implemented in base class")
383 raise NotImplementedError("Not implemented in base class")
384
384
385 def auth(self, userobj, username, passwd, settings, **kwargs):
385 def auth(self, userobj, username, passwd, settings, **kwargs):
386 """
386 """
387 Given a user object (which may be null), username, a plaintext
387 Given a user object (which may be null), username, a plaintext
388 password, and a settings object (containing all the keys needed as
388 password, and a settings object (containing all the keys needed as
389 listed in settings()), authenticate this user's login attempt.
389 listed in settings()), authenticate this user's login attempt.
390
390
391 Return None on failure. On success, return a dictionary of the form:
391 Return None on failure. On success, return a dictionary of the form:
392
392
393 see: RhodeCodeAuthPluginBase.auth_func_attrs
393 see: RhodeCodeAuthPluginBase.auth_func_attrs
394 This is later validated for correctness
394 This is later validated for correctness
395 """
395 """
396 raise NotImplementedError("not implemented in base class")
396 raise NotImplementedError("not implemented in base class")
397
397
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
398 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
399 """
399 """
400 Wrapper to call self.auth() that validates call on it
400 Wrapper to call self.auth() that validates call on it
401
401
402 :param userobj: userobj
402 :param userobj: userobj
403 :param username: username
403 :param username: username
404 :param passwd: plaintext password
404 :param passwd: plaintext password
405 :param settings: plugin settings
405 :param settings: plugin settings
406 """
406 """
407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
407 auth = self.auth(userobj, username, passwd, settings, **kwargs)
408 if auth:
408 if auth:
409 auth['_plugin'] = self.name
409 auth['_plugin'] = self.name
410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
410 auth['_ttl_cache'] = self.get_ttl_cache(settings)
411 # check if hash should be migrated ?
411 # check if hash should be migrated ?
412 new_hash = auth.get('_hash_migrate')
412 new_hash = auth.get('_hash_migrate')
413 if new_hash:
413 if new_hash:
414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
414 self._migrate_hash_to_bcrypt(username, passwd, new_hash)
415 if 'user_group_sync' not in auth:
415 if 'user_group_sync' not in auth:
416 auth['user_group_sync'] = False
416 auth['user_group_sync'] = False
417 return self._validate_auth_return(auth)
417 return self._validate_auth_return(auth)
418 return auth
418 return auth
419
419
420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
420 def _migrate_hash_to_bcrypt(self, username, password, new_hash):
421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
421 new_hash_cypher = _RhodeCodeCryptoBCrypt()
422 # extra checks, so make sure new hash is correct.
422 # extra checks, so make sure new hash is correct.
423 password_encoded = safe_str(password)
423 password_encoded = safe_str(password)
424 if new_hash and new_hash_cypher.hash_check(
424 if new_hash and new_hash_cypher.hash_check(
425 password_encoded, new_hash):
425 password_encoded, new_hash):
426 cur_user = User.get_by_username(username)
426 cur_user = User.get_by_username(username)
427 cur_user.password = new_hash
427 cur_user.password = new_hash
428 Session().add(cur_user)
428 Session().add(cur_user)
429 Session().flush()
429 Session().flush()
430 log.info('Migrated user %s hash to bcrypt', cur_user)
430 log.info('Migrated user %s hash to bcrypt', cur_user)
431
431
432 def _validate_auth_return(self, ret):
432 def _validate_auth_return(self, ret):
433 if not isinstance(ret, dict):
433 if not isinstance(ret, dict):
434 raise Exception('returned value from auth must be a dict')
434 raise Exception('returned value from auth must be a dict')
435 for k in self.auth_func_attrs:
435 for k in self.auth_func_attrs:
436 if k not in ret:
436 if k not in ret:
437 raise Exception('Missing %s attribute from returned data' % k)
437 raise Exception('Missing %s attribute from returned data' % k)
438 return ret
438 return ret
439
439
440 def get_ttl_cache(self, settings=None):
440 def get_ttl_cache(self, settings=None):
441 plugin_settings = settings or self.get_settings()
441 plugin_settings = settings or self.get_settings()
442 cache_ttl = 0
442 cache_ttl = 0
443
443
444 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
444 if isinstance(self.AUTH_CACHE_TTL, (int, long)):
445 # plugin cache set inside is more important than the settings value
445 # plugin cache set inside is more important than the settings value
446 cache_ttl = self.AUTH_CACHE_TTL
446 cache_ttl = self.AUTH_CACHE_TTL
447 elif plugin_settings.get('cache_ttl'):
447 elif plugin_settings.get('cache_ttl'):
448 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
448 cache_ttl = safe_int(plugin_settings.get('cache_ttl'), 0)
449
449
450 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
450 plugin_cache_active = bool(cache_ttl and cache_ttl > 0)
451 return plugin_cache_active, cache_ttl
451 return plugin_cache_active, cache_ttl
452
452
453
453
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
454 class RhodeCodeExternalAuthPlugin(RhodeCodeAuthPluginBase):
455
455
456 @hybrid_property
456 @hybrid_property
457 def allows_creating_users(self):
457 def allows_creating_users(self):
458 return True
458 return True
459
459
460 def use_fake_password(self):
460 def use_fake_password(self):
461 """
461 """
462 Return a boolean that indicates whether or not we should set the user's
462 Return a boolean that indicates whether or not we should set the user's
463 password to a random value when it is authenticated by this plugin.
463 password to a random value when it is authenticated by this plugin.
464 If your plugin provides authentication, then you will generally
464 If your plugin provides authentication, then you will generally
465 want this.
465 want this.
466
466
467 :returns: boolean
467 :returns: boolean
468 """
468 """
469 raise NotImplementedError("Not implemented in base class")
469 raise NotImplementedError("Not implemented in base class")
470
470
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
471 def _authenticate(self, userobj, username, passwd, settings, **kwargs):
472 # at this point _authenticate calls plugin's `auth()` function
472 # at this point _authenticate calls plugin's `auth()` function
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
473 auth = super(RhodeCodeExternalAuthPlugin, self)._authenticate(
474 userobj, username, passwd, settings, **kwargs)
474 userobj, username, passwd, settings, **kwargs)
475
475
476 if auth:
476 if auth:
477 # maybe plugin will clean the username ?
477 # maybe plugin will clean the username ?
478 # we should use the return value
478 # we should use the return value
479 username = auth['username']
479 username = auth['username']
480
480
481 # if external source tells us that user is not active, we should
481 # if external source tells us that user is not active, we should
482 # skip rest of the process. This can prevent from creating users in
482 # skip rest of the process. This can prevent from creating users in
483 # RhodeCode when using external authentication, but if it's
483 # RhodeCode when using external authentication, but if it's
484 # inactive user we shouldn't create that user anyway
484 # inactive user we shouldn't create that user anyway
485 if auth['active_from_extern'] is False:
485 if auth['active_from_extern'] is False:
486 log.warning(
486 log.warning(
487 "User %s authenticated against %s, but is inactive",
487 "User %s authenticated against %s, but is inactive",
488 username, self.__module__)
488 username, self.__module__)
489 return None
489 return None
490
490
491 cur_user = User.get_by_username(username, case_insensitive=True)
491 cur_user = User.get_by_username(username, case_insensitive=True)
492 is_user_existing = cur_user is not None
492 is_user_existing = cur_user is not None
493
493
494 if is_user_existing:
494 if is_user_existing:
495 log.debug('Syncing user `%s` from '
495 log.debug('Syncing user `%s` from '
496 '`%s` plugin', username, self.name)
496 '`%s` plugin', username, self.name)
497 else:
497 else:
498 log.debug('Creating non existing user `%s` from '
498 log.debug('Creating non existing user `%s` from '
499 '`%s` plugin', username, self.name)
499 '`%s` plugin', username, self.name)
500
500
501 if self.allows_creating_users:
501 if self.allows_creating_users:
502 log.debug('Plugin `%s` allows to '
502 log.debug('Plugin `%s` allows to '
503 'create new users', self.name)
503 'create new users', self.name)
504 else:
504 else:
505 log.debug('Plugin `%s` does not allow to '
505 log.debug('Plugin `%s` does not allow to '
506 'create new users', self.name)
506 'create new users', self.name)
507
507
508 user_parameters = {
508 user_parameters = {
509 'username': username,
509 'username': username,
510 'email': auth["email"],
510 'email': auth["email"],
511 'firstname': auth["firstname"],
511 'firstname': auth["firstname"],
512 'lastname': auth["lastname"],
512 'lastname': auth["lastname"],
513 'active': auth["active"],
513 'active': auth["active"],
514 'admin': auth["admin"],
514 'admin': auth["admin"],
515 'extern_name': auth["extern_name"],
515 'extern_name': auth["extern_name"],
516 'extern_type': self.name,
516 'extern_type': self.name,
517 'plugin': self,
517 'plugin': self,
518 'allow_to_create_user': self.allows_creating_users,
518 'allow_to_create_user': self.allows_creating_users,
519 }
519 }
520
520
521 if not is_user_existing:
521 if not is_user_existing:
522 if self.use_fake_password():
522 if self.use_fake_password():
523 # Randomize the PW because we don't need it, but don't want
523 # Randomize the PW because we don't need it, but don't want
524 # them blank either
524 # them blank either
525 passwd = PasswordGenerator().gen_password(length=16)
525 passwd = PasswordGenerator().gen_password(length=16)
526 user_parameters['password'] = passwd
526 user_parameters['password'] = passwd
527 else:
527 else:
528 # Since the password is required by create_or_update method of
528 # Since the password is required by create_or_update method of
529 # UserModel, we need to set it explicitly.
529 # UserModel, we need to set it explicitly.
530 # The create_or_update method is smart and recognises the
530 # The create_or_update method is smart and recognises the
531 # password hashes as well.
531 # password hashes as well.
532 user_parameters['password'] = cur_user.password
532 user_parameters['password'] = cur_user.password
533
533
534 # we either create or update users, we also pass the flag
534 # we either create or update users, we also pass the flag
535 # that controls if this method can actually do that.
535 # that controls if this method can actually do that.
536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
536 # raises NotAllowedToCreateUserError if it cannot, and we try to.
537 user = UserModel().create_or_update(**user_parameters)
537 user = UserModel().create_or_update(**user_parameters)
538 Session().flush()
538 Session().flush()
539 # enforce user is just in given groups, all of them has to be ones
539 # enforce user is just in given groups, all of them has to be ones
540 # created from plugins. We store this info in _group_data JSON
540 # created from plugins. We store this info in _group_data JSON
541 # field
541 # field
542
542
543 if auth['user_group_sync']:
543 if auth['user_group_sync']:
544 try:
544 try:
545 groups = auth['groups'] or []
545 groups = auth['groups'] or []
546 log.debug(
546 log.debug(
547 'Performing user_group sync based on set `%s` '
547 'Performing user_group sync based on set `%s` '
548 'returned by `%s` plugin', groups, self.name)
548 'returned by `%s` plugin', groups, self.name)
549 UserGroupModel().enforce_groups(user, groups, self.name)
549 UserGroupModel().enforce_groups(user, groups, self.name)
550 except Exception:
550 except Exception:
551 # for any reason group syncing fails, we should
551 # for any reason group syncing fails, we should
552 # proceed with login
552 # proceed with login
553 log.error(traceback.format_exc())
553 log.error(traceback.format_exc())
554
554
555 Session().commit()
555 Session().commit()
556 return auth
556 return auth
557
557
558
558
559 class AuthLdapBase(object):
559 class AuthLdapBase(object):
560
560
561 @classmethod
561 @classmethod
562 def _build_servers(cls, ldap_server_type, ldap_server, port):
562 def _build_servers(cls, ldap_server_type, ldap_server, port):
563 def host_resolver(host, port, full_resolve=True):
563 def host_resolver(host, port, full_resolve=True):
564 """
564 """
565 Main work for this function is to prevent ldap connection issues,
565 Main work for this function is to prevent ldap connection issues,
566 and detect them early using a "greenified" sockets
566 and detect them early using a "greenified" sockets
567 """
567 """
568 host = host.strip()
568 host = host.strip()
569 if not full_resolve:
569 if not full_resolve:
570 return '{}:{}'.format(host, port)
570 return '{}:{}'.format(host, port)
571
571
572 log.debug('LDAP: Resolving IP for LDAP host %s', host)
572 log.debug('LDAP: Resolving IP for LDAP host %s', host)
573 try:
573 try:
574 ip = socket.gethostbyname(host)
574 ip = socket.gethostbyname(host)
575 log.debug('Got LDAP server %s ip %s', host, ip)
575 log.debug('Got LDAP server %s ip %s', host, ip)
576 except Exception:
576 except Exception:
577 raise LdapConnectionError(
577 raise LdapConnectionError(
578 'Failed to resolve host: `{}`'.format(host))
578 'Failed to resolve host: `{}`'.format(host))
579
579
580 log.debug('LDAP: Checking if IP %s is accessible', ip)
580 log.debug('LDAP: Checking if IP %s is accessible', ip)
581 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
581 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
582 try:
582 try:
583 s.connect((ip, int(port)))
583 s.connect((ip, int(port)))
584 s.shutdown(socket.SHUT_RD)
584 s.shutdown(socket.SHUT_RD)
585 except Exception:
585 except Exception:
586 raise LdapConnectionError(
586 raise LdapConnectionError(
587 'Failed to connect to host: `{}:{}`'.format(host, port))
587 'Failed to connect to host: `{}:{}`'.format(host, port))
588
588
589 return '{}:{}'.format(host, port)
589 return '{}:{}'.format(host, port)
590
590
591 if len(ldap_server) == 1:
591 if len(ldap_server) == 1:
592 # in case of single server use resolver to detect potential
592 # in case of single server use resolver to detect potential
593 # connection issues
593 # connection issues
594 full_resolve = True
594 full_resolve = True
595 else:
595 else:
596 full_resolve = False
596 full_resolve = False
597
597
598 return ', '.join(
598 return ', '.join(
599 ["{}://{}".format(
599 ["{}://{}".format(
600 ldap_server_type,
600 ldap_server_type,
601 host_resolver(host, port, full_resolve=full_resolve))
601 host_resolver(host, port, full_resolve=full_resolve))
602 for host in ldap_server])
602 for host in ldap_server])
603
603
604 @classmethod
604 @classmethod
605 def _get_server_list(cls, servers):
605 def _get_server_list(cls, servers):
606 return map(string.strip, servers.split(','))
606 return map(string.strip, servers.split(','))
607
607
608 @classmethod
608 @classmethod
609 def get_uid(cls, username, server_addresses):
609 def get_uid(cls, username, server_addresses):
610 uid = username
610 uid = username
611 for server_addr in server_addresses:
611 for server_addr in server_addresses:
612 uid = chop_at(username, "@%s" % server_addr)
612 uid = chop_at(username, "@%s" % server_addr)
613 return uid
613 return uid
614
614
615
615
616 def loadplugin(plugin_id):
616 def loadplugin(plugin_id):
617 """
617 """
618 Loads and returns an instantiated authentication plugin.
618 Loads and returns an instantiated authentication plugin.
619 Returns the RhodeCodeAuthPluginBase subclass on success,
619 Returns the RhodeCodeAuthPluginBase subclass on success,
620 or None on failure.
620 or None on failure.
621 """
621 """
622 # TODO: Disusing pyramids thread locals to retrieve the registry.
622 # TODO: Disusing pyramids thread locals to retrieve the registry.
623 authn_registry = get_authn_registry()
623 authn_registry = get_authn_registry()
624 plugin = authn_registry.get_plugin(plugin_id)
624 plugin = authn_registry.get_plugin(plugin_id)
625 if plugin is None:
625 if plugin is None:
626 log.error('Authentication plugin not found: "%s"', plugin_id)
626 log.error('Authentication plugin not found: "%s"', plugin_id)
627 return plugin
627 return plugin
628
628
629
629
630 def get_authn_registry(registry=None):
630 def get_authn_registry(registry=None):
631 registry = registry or get_current_registry()
631 registry = registry or get_current_registry()
632 authn_registry = registry.getUtility(IAuthnPluginRegistry)
632 authn_registry = registry.getUtility(IAuthnPluginRegistry)
633 return authn_registry
633 return authn_registry
634
634
635
635
636 def authenticate(username, password, environ=None, auth_type=None,
636 def authenticate(username, password, environ=None, auth_type=None,
637 skip_missing=False, registry=None, acl_repo_name=None):
637 skip_missing=False, registry=None, acl_repo_name=None):
638 """
638 """
639 Authentication function used for access control,
639 Authentication function used for access control,
640 It tries to authenticate based on enabled authentication modules.
640 It tries to authenticate based on enabled authentication modules.
641
641
642 :param username: username can be empty for headers auth
642 :param username: username can be empty for headers auth
643 :param password: password can be empty for headers auth
643 :param password: password can be empty for headers auth
644 :param environ: environ headers passed for headers auth
644 :param environ: environ headers passed for headers auth
645 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
645 :param auth_type: type of authentication, either `HTTP_TYPE` or `VCS_TYPE`
646 :param skip_missing: ignores plugins that are in db but not in environment
646 :param skip_missing: ignores plugins that are in db but not in environment
647 :returns: None if auth failed, plugin_user dict if auth is correct
647 :returns: None if auth failed, plugin_user dict if auth is correct
648 """
648 """
649 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
649 if not auth_type or auth_type not in [HTTP_TYPE, VCS_TYPE]:
650 raise ValueError('auth type must be on of http, vcs got "%s" instead'
650 raise ValueError('auth type must be on of http, vcs got "%s" instead'
651 % auth_type)
651 % auth_type)
652 headers_only = environ and not (username and password)
652 headers_only = environ and not (username and password)
653
653
654 authn_registry = get_authn_registry(registry)
654 authn_registry = get_authn_registry(registry)
655 plugins_to_check = authn_registry.get_plugins_for_authentication()
655 plugins_to_check = authn_registry.get_plugins_for_authentication()
656 log.debug('Starting ordered authentication chain using %s plugins',
656 log.debug('Starting ordered authentication chain using %s plugins',
657 [x.name for x in plugins_to_check])
657 [x.name for x in plugins_to_check])
658 for plugin in plugins_to_check:
658 for plugin in plugins_to_check:
659 plugin.set_auth_type(auth_type)
659 plugin.set_auth_type(auth_type)
660 plugin.set_calling_scope_repo(acl_repo_name)
660 plugin.set_calling_scope_repo(acl_repo_name)
661
661
662 if headers_only and not plugin.is_headers_auth:
662 if headers_only and not plugin.is_headers_auth:
663 log.debug('Auth type is for headers only and plugin `%s` is not '
663 log.debug('Auth type is for headers only and plugin `%s` is not '
664 'headers plugin, skipping...', plugin.get_id())
664 'headers plugin, skipping...', plugin.get_id())
665 continue
665 continue
666
666
667 log.debug('Trying authentication using ** %s **', plugin.get_id())
667 log.debug('Trying authentication using ** %s **', plugin.get_id())
668
668
669 # load plugin settings from RhodeCode database
669 # load plugin settings from RhodeCode database
670 plugin_settings = plugin.get_settings()
670 plugin_settings = plugin.get_settings()
671 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
671 plugin_sanitized_settings = plugin.log_safe_settings(plugin_settings)
672 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
672 log.debug('Plugin `%s` settings:%s', plugin.get_id(), plugin_sanitized_settings)
673
673
674 # use plugin's method of user extraction.
674 # use plugin's method of user extraction.
675 user = plugin.get_user(username, environ=environ,
675 user = plugin.get_user(username, environ=environ,
676 settings=plugin_settings)
676 settings=plugin_settings)
677 display_user = user.username if user else username
677 display_user = user.username if user else username
678 log.debug(
678 log.debug(
679 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
679 'Plugin %s extracted user is `%s`', plugin.get_id(), display_user)
680
680
681 if not plugin.allows_authentication_from(user):
681 if not plugin.allows_authentication_from(user):
682 log.debug('Plugin %s does not accept user `%s` for authentication',
682 log.debug('Plugin %s does not accept user `%s` for authentication',
683 plugin.get_id(), display_user)
683 plugin.get_id(), display_user)
684 continue
684 continue
685 else:
685 else:
686 log.debug('Plugin %s accepted user `%s` for authentication',
686 log.debug('Plugin %s accepted user `%s` for authentication',
687 plugin.get_id(), display_user)
687 plugin.get_id(), display_user)
688
688
689 log.info('Authenticating user `%s` using %s plugin',
689 log.info('Authenticating user `%s` using %s plugin',
690 display_user, plugin.get_id())
690 display_user, plugin.get_id())
691
691
692 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
692 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(plugin_settings)
693
693
694 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
694 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
695 plugin.get_id(), plugin_cache_active, cache_ttl)
695 plugin.get_id(), plugin_cache_active, cache_ttl)
696
696
697 user_id = user.user_id if user else None
697 user_id = user.user_id if user else None
698 # don't cache for empty users
698 # don't cache for empty users
699 plugin_cache_active = plugin_cache_active and user_id
699 plugin_cache_active = plugin_cache_active and user_id
700 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
700 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
701 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
701 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
702
702
703 @region.cache_on_arguments(namespace=cache_namespace_uid,
703 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
704 expiration_time=cache_ttl,
704 expiration_time=cache_ttl,
705 should_cache_fn=lambda v: plugin_cache_active)
705 condition=plugin_cache_active)
706 def compute_auth(
706 def compute_auth(
707 cache_name, plugin_name, username, password):
707 cache_name, plugin_name, username, password):
708
708
709 # _authenticate is a wrapper for .auth() method of plugin.
709 # _authenticate is a wrapper for .auth() method of plugin.
710 # it checks if .auth() sends proper data.
710 # it checks if .auth() sends proper data.
711 # For RhodeCodeExternalAuthPlugin it also maps users to
711 # For RhodeCodeExternalAuthPlugin it also maps users to
712 # Database and maps the attributes returned from .auth()
712 # Database and maps the attributes returned from .auth()
713 # to RhodeCode database. If this function returns data
713 # to RhodeCode database. If this function returns data
714 # then auth is correct.
714 # then auth is correct.
715 log.debug('Running plugin `%s` _authenticate method '
715 log.debug('Running plugin `%s` _authenticate method '
716 'using username and password', plugin.get_id())
716 'using username and password', plugin.get_id())
717 return plugin._authenticate(
717 return plugin._authenticate(
718 user, username, password, plugin_settings,
718 user, username, password, plugin_settings,
719 environ=environ or {})
719 environ=environ or {})
720
720
721 start = time.time()
721 start = time.time()
722 # for environ based auth, password can be empty, but then the validation is
722 # for environ based auth, password can be empty, but then the validation is
723 # on the server that fills in the env data needed for authentication
723 # on the server that fills in the env data needed for authentication
724 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
724 plugin_user = compute_auth('auth', plugin.name, username, (password or ''))
725
725
726 auth_time = time.time() - start
726 auth_time = time.time() - start
727 log.debug('Authentication for plugin `%s` completed in %.3fs, '
727 log.debug('Authentication for plugin `%s` completed in %.3fs, '
728 'expiration time of fetched cache %.1fs.',
728 'expiration time of fetched cache %.1fs.',
729 plugin.get_id(), auth_time, cache_ttl)
729 plugin.get_id(), auth_time, cache_ttl)
730
730
731 log.debug('PLUGIN USER DATA: %s', plugin_user)
731 log.debug('PLUGIN USER DATA: %s', plugin_user)
732
732
733 if plugin_user:
733 if plugin_user:
734 log.debug('Plugin returned proper authentication data')
734 log.debug('Plugin returned proper authentication data')
735 return plugin_user
735 return plugin_user
736 # we failed to Auth because .auth() method didn't return proper user
736 # we failed to Auth because .auth() method didn't return proper user
737 log.debug("User `%s` failed to authenticate against %s",
737 log.debug("User `%s` failed to authenticate against %s",
738 display_user, plugin.get_id())
738 display_user, plugin.get_id())
739
739
740 # case when we failed to authenticate against all defined plugins
740 # case when we failed to authenticate against all defined plugins
741 return None
741 return None
742
742
743
743
744 def chop_at(s, sub, inclusive=False):
744 def chop_at(s, sub, inclusive=False):
745 """Truncate string ``s`` at the first occurrence of ``sub``.
745 """Truncate string ``s`` at the first occurrence of ``sub``.
746
746
747 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
747 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
748
748
749 >>> chop_at("plutocratic brats", "rat")
749 >>> chop_at("plutocratic brats", "rat")
750 'plutoc'
750 'plutoc'
751 >>> chop_at("plutocratic brats", "rat", True)
751 >>> chop_at("plutocratic brats", "rat", True)
752 'plutocrat'
752 'plutocrat'
753 """
753 """
754 pos = s.find(sub)
754 pos = s.find(sub)
755 if pos == -1:
755 if pos == -1:
756 return s
756 return s
757 if inclusive:
757 if inclusive:
758 return s[:pos+len(sub)]
758 return s[:pos+len(sub)]
759 return s[:pos]
759 return s[:pos]
@@ -1,2202 +1,2202 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 authentication and permission libraries
22 authentication and permission libraries
23 """
23 """
24
24
25 import os
25 import os
26 import time
26 import time
27 import inspect
27 import inspect
28 import collections
28 import collections
29 import fnmatch
29 import fnmatch
30 import hashlib
30 import hashlib
31 import itertools
31 import itertools
32 import logging
32 import logging
33 import random
33 import random
34 import traceback
34 import traceback
35 from functools import wraps
35 from functools import wraps
36
36
37 import ipaddress
37 import ipaddress
38
38
39 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
39 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
40 from sqlalchemy.orm.exc import ObjectDeletedError
40 from sqlalchemy.orm.exc import ObjectDeletedError
41 from sqlalchemy.orm import joinedload
41 from sqlalchemy.orm import joinedload
42 from zope.cachedescriptors.property import Lazy as LazyProperty
42 from zope.cachedescriptors.property import Lazy as LazyProperty
43
43
44 import rhodecode
44 import rhodecode
45 from rhodecode.model import meta
45 from rhodecode.model import meta
46 from rhodecode.model.meta import Session
46 from rhodecode.model.meta import Session
47 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
49 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
50 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
50 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
51 from rhodecode.lib import rc_cache
51 from rhodecode.lib import rc_cache
52 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
52 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
53 from rhodecode.lib.utils import (
53 from rhodecode.lib.utils import (
54 get_repo_slug, get_repo_group_slug, get_user_group_slug)
54 get_repo_slug, get_repo_group_slug, get_user_group_slug)
55 from rhodecode.lib.caching_query import FromCache
55 from rhodecode.lib.caching_query import FromCache
56
56
57
57
58 if rhodecode.is_unix:
58 if rhodecode.is_unix:
59 import bcrypt
59 import bcrypt
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63 csrf_token_key = "csrf_token"
63 csrf_token_key = "csrf_token"
64
64
65
65
66 class PasswordGenerator(object):
66 class PasswordGenerator(object):
67 """
67 """
68 This is a simple class for generating password from different sets of
68 This is a simple class for generating password from different sets of
69 characters
69 characters
70 usage::
70 usage::
71
71
72 passwd_gen = PasswordGenerator()
72 passwd_gen = PasswordGenerator()
73 #print 8-letter password containing only big and small letters
73 #print 8-letter password containing only big and small letters
74 of alphabet
74 of alphabet
75 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
76 """
76 """
77 ALPHABETS_NUM = r'''1234567890'''
77 ALPHABETS_NUM = r'''1234567890'''
78 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
79 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
80 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
81 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
82 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 + ALPHABETS_NUM + ALPHABETS_SPECIAL
83 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
84 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
85 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
86 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
87
87
88 def __init__(self, passwd=''):
88 def __init__(self, passwd=''):
89 self.passwd = passwd
89 self.passwd = passwd
90
90
91 def gen_password(self, length, type_=None):
91 def gen_password(self, length, type_=None):
92 if type_ is None:
92 if type_ is None:
93 type_ = self.ALPHABETS_FULL
93 type_ = self.ALPHABETS_FULL
94 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
94 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
95 return self.passwd
95 return self.passwd
96
96
97
97
98 class _RhodeCodeCryptoBase(object):
98 class _RhodeCodeCryptoBase(object):
99 ENC_PREF = None
99 ENC_PREF = None
100
100
101 def hash_create(self, str_):
101 def hash_create(self, str_):
102 """
102 """
103 hash the string using
103 hash the string using
104
104
105 :param str_: password to hash
105 :param str_: password to hash
106 """
106 """
107 raise NotImplementedError
107 raise NotImplementedError
108
108
109 def hash_check_with_upgrade(self, password, hashed):
109 def hash_check_with_upgrade(self, password, hashed):
110 """
110 """
111 Returns tuple in which first element is boolean that states that
111 Returns tuple in which first element is boolean that states that
112 given password matches it's hashed version, and the second is new hash
112 given password matches it's hashed version, and the second is new hash
113 of the password, in case this password should be migrated to new
113 of the password, in case this password should be migrated to new
114 cipher.
114 cipher.
115 """
115 """
116 checked_hash = self.hash_check(password, hashed)
116 checked_hash = self.hash_check(password, hashed)
117 return checked_hash, None
117 return checked_hash, None
118
118
119 def hash_check(self, password, hashed):
119 def hash_check(self, password, hashed):
120 """
120 """
121 Checks matching password with it's hashed value.
121 Checks matching password with it's hashed value.
122
122
123 :param password: password
123 :param password: password
124 :param hashed: password in hashed form
124 :param hashed: password in hashed form
125 """
125 """
126 raise NotImplementedError
126 raise NotImplementedError
127
127
128 def _assert_bytes(self, value):
128 def _assert_bytes(self, value):
129 """
129 """
130 Passing in an `unicode` object can lead to hard to detect issues
130 Passing in an `unicode` object can lead to hard to detect issues
131 if passwords contain non-ascii characters. Doing a type check
131 if passwords contain non-ascii characters. Doing a type check
132 during runtime, so that such mistakes are detected early on.
132 during runtime, so that such mistakes are detected early on.
133 """
133 """
134 if not isinstance(value, str):
134 if not isinstance(value, str):
135 raise TypeError(
135 raise TypeError(
136 "Bytestring required as input, got %r." % (value, ))
136 "Bytestring required as input, got %r." % (value, ))
137
137
138
138
139 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
139 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
140 ENC_PREF = ('$2a$10', '$2b$10')
140 ENC_PREF = ('$2a$10', '$2b$10')
141
141
142 def hash_create(self, str_):
142 def hash_create(self, str_):
143 self._assert_bytes(str_)
143 self._assert_bytes(str_)
144 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
144 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
145
145
146 def hash_check_with_upgrade(self, password, hashed):
146 def hash_check_with_upgrade(self, password, hashed):
147 """
147 """
148 Returns tuple in which first element is boolean that states that
148 Returns tuple in which first element is boolean that states that
149 given password matches it's hashed version, and the second is new hash
149 given password matches it's hashed version, and the second is new hash
150 of the password, in case this password should be migrated to new
150 of the password, in case this password should be migrated to new
151 cipher.
151 cipher.
152
152
153 This implements special upgrade logic which works like that:
153 This implements special upgrade logic which works like that:
154 - check if the given password == bcrypted hash, if yes then we
154 - check if the given password == bcrypted hash, if yes then we
155 properly used password and it was already in bcrypt. Proceed
155 properly used password and it was already in bcrypt. Proceed
156 without any changes
156 without any changes
157 - if bcrypt hash check is not working try with sha256. If hash compare
157 - if bcrypt hash check is not working try with sha256. If hash compare
158 is ok, it means we using correct but old hashed password. indicate
158 is ok, it means we using correct but old hashed password. indicate
159 hash change and proceed
159 hash change and proceed
160 """
160 """
161
161
162 new_hash = None
162 new_hash = None
163
163
164 # regular pw check
164 # regular pw check
165 password_match_bcrypt = self.hash_check(password, hashed)
165 password_match_bcrypt = self.hash_check(password, hashed)
166
166
167 # now we want to know if the password was maybe from sha256
167 # now we want to know if the password was maybe from sha256
168 # basically calling _RhodeCodeCryptoSha256().hash_check()
168 # basically calling _RhodeCodeCryptoSha256().hash_check()
169 if not password_match_bcrypt:
169 if not password_match_bcrypt:
170 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
170 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
171 new_hash = self.hash_create(password) # make new bcrypt hash
171 new_hash = self.hash_create(password) # make new bcrypt hash
172 password_match_bcrypt = True
172 password_match_bcrypt = True
173
173
174 return password_match_bcrypt, new_hash
174 return password_match_bcrypt, new_hash
175
175
176 def hash_check(self, password, hashed):
176 def hash_check(self, password, hashed):
177 """
177 """
178 Checks matching password with it's hashed value.
178 Checks matching password with it's hashed value.
179
179
180 :param password: password
180 :param password: password
181 :param hashed: password in hashed form
181 :param hashed: password in hashed form
182 """
182 """
183 self._assert_bytes(password)
183 self._assert_bytes(password)
184 try:
184 try:
185 return bcrypt.hashpw(password, hashed) == hashed
185 return bcrypt.hashpw(password, hashed) == hashed
186 except ValueError as e:
186 except ValueError as e:
187 # we're having a invalid salt here probably, we should not crash
187 # we're having a invalid salt here probably, we should not crash
188 # just return with False as it would be a wrong password.
188 # just return with False as it would be a wrong password.
189 log.debug('Failed to check password hash using bcrypt %s',
189 log.debug('Failed to check password hash using bcrypt %s',
190 safe_str(e))
190 safe_str(e))
191
191
192 return False
192 return False
193
193
194
194
195 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
195 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
196 ENC_PREF = '_'
196 ENC_PREF = '_'
197
197
198 def hash_create(self, str_):
198 def hash_create(self, str_):
199 self._assert_bytes(str_)
199 self._assert_bytes(str_)
200 return hashlib.sha256(str_).hexdigest()
200 return hashlib.sha256(str_).hexdigest()
201
201
202 def hash_check(self, password, hashed):
202 def hash_check(self, password, hashed):
203 """
203 """
204 Checks matching password with it's hashed value.
204 Checks matching password with it's hashed value.
205
205
206 :param password: password
206 :param password: password
207 :param hashed: password in hashed form
207 :param hashed: password in hashed form
208 """
208 """
209 self._assert_bytes(password)
209 self._assert_bytes(password)
210 return hashlib.sha256(password).hexdigest() == hashed
210 return hashlib.sha256(password).hexdigest() == hashed
211
211
212
212
213 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
213 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
214 ENC_PREF = '_'
214 ENC_PREF = '_'
215
215
216 def hash_create(self, str_):
216 def hash_create(self, str_):
217 self._assert_bytes(str_)
217 self._assert_bytes(str_)
218 return sha1(str_)
218 return sha1(str_)
219
219
220 def hash_check(self, password, hashed):
220 def hash_check(self, password, hashed):
221 """
221 """
222 Checks matching password with it's hashed value.
222 Checks matching password with it's hashed value.
223
223
224 :param password: password
224 :param password: password
225 :param hashed: password in hashed form
225 :param hashed: password in hashed form
226 """
226 """
227 self._assert_bytes(password)
227 self._assert_bytes(password)
228 return sha1(password) == hashed
228 return sha1(password) == hashed
229
229
230
230
231 def crypto_backend():
231 def crypto_backend():
232 """
232 """
233 Return the matching crypto backend.
233 Return the matching crypto backend.
234
234
235 Selection is based on if we run tests or not, we pick sha1-test backend to run
235 Selection is based on if we run tests or not, we pick sha1-test backend to run
236 tests faster since BCRYPT is expensive to calculate
236 tests faster since BCRYPT is expensive to calculate
237 """
237 """
238 if rhodecode.is_test:
238 if rhodecode.is_test:
239 RhodeCodeCrypto = _RhodeCodeCryptoTest()
239 RhodeCodeCrypto = _RhodeCodeCryptoTest()
240 else:
240 else:
241 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
242
242
243 return RhodeCodeCrypto
243 return RhodeCodeCrypto
244
244
245
245
246 def get_crypt_password(password):
246 def get_crypt_password(password):
247 """
247 """
248 Create the hash of `password` with the active crypto backend.
248 Create the hash of `password` with the active crypto backend.
249
249
250 :param password: The cleartext password.
250 :param password: The cleartext password.
251 :type password: unicode
251 :type password: unicode
252 """
252 """
253 password = safe_str(password)
253 password = safe_str(password)
254 return crypto_backend().hash_create(password)
254 return crypto_backend().hash_create(password)
255
255
256
256
257 def check_password(password, hashed):
257 def check_password(password, hashed):
258 """
258 """
259 Check if the value in `password` matches the hash in `hashed`.
259 Check if the value in `password` matches the hash in `hashed`.
260
260
261 :param password: The cleartext password.
261 :param password: The cleartext password.
262 :type password: unicode
262 :type password: unicode
263
263
264 :param hashed: The expected hashed version of the password.
264 :param hashed: The expected hashed version of the password.
265 :type hashed: The hash has to be passed in in text representation.
265 :type hashed: The hash has to be passed in in text representation.
266 """
266 """
267 password = safe_str(password)
267 password = safe_str(password)
268 return crypto_backend().hash_check(password, hashed)
268 return crypto_backend().hash_check(password, hashed)
269
269
270
270
271 def generate_auth_token(data, salt=None):
271 def generate_auth_token(data, salt=None):
272 """
272 """
273 Generates API KEY from given string
273 Generates API KEY from given string
274 """
274 """
275
275
276 if salt is None:
276 if salt is None:
277 salt = os.urandom(16)
277 salt = os.urandom(16)
278 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 return hashlib.sha1(safe_str(data) + salt).hexdigest()
279
279
280
280
281 def get_came_from(request):
281 def get_came_from(request):
282 """
282 """
283 get query_string+path from request sanitized after removing auth_token
283 get query_string+path from request sanitized after removing auth_token
284 """
284 """
285 _req = request
285 _req = request
286
286
287 path = _req.path
287 path = _req.path
288 if 'auth_token' in _req.GET:
288 if 'auth_token' in _req.GET:
289 # sanitize the request and remove auth_token for redirection
289 # sanitize the request and remove auth_token for redirection
290 _req.GET.pop('auth_token')
290 _req.GET.pop('auth_token')
291 qs = _req.query_string
291 qs = _req.query_string
292 if qs:
292 if qs:
293 path += '?' + qs
293 path += '?' + qs
294
294
295 return path
295 return path
296
296
297
297
298 class CookieStoreWrapper(object):
298 class CookieStoreWrapper(object):
299
299
300 def __init__(self, cookie_store):
300 def __init__(self, cookie_store):
301 self.cookie_store = cookie_store
301 self.cookie_store = cookie_store
302
302
303 def __repr__(self):
303 def __repr__(self):
304 return 'CookieStore<%s>' % (self.cookie_store)
304 return 'CookieStore<%s>' % (self.cookie_store)
305
305
306 def get(self, key, other=None):
306 def get(self, key, other=None):
307 if isinstance(self.cookie_store, dict):
307 if isinstance(self.cookie_store, dict):
308 return self.cookie_store.get(key, other)
308 return self.cookie_store.get(key, other)
309 elif isinstance(self.cookie_store, AuthUser):
309 elif isinstance(self.cookie_store, AuthUser):
310 return self.cookie_store.__dict__.get(key, other)
310 return self.cookie_store.__dict__.get(key, other)
311
311
312
312
313 def _cached_perms_data(user_id, scope, user_is_admin,
313 def _cached_perms_data(user_id, scope, user_is_admin,
314 user_inherit_default_permissions, explicit, algo,
314 user_inherit_default_permissions, explicit, algo,
315 calculate_super_admin):
315 calculate_super_admin):
316
316
317 permissions = PermissionCalculator(
317 permissions = PermissionCalculator(
318 user_id, scope, user_is_admin, user_inherit_default_permissions,
318 user_id, scope, user_is_admin, user_inherit_default_permissions,
319 explicit, algo, calculate_super_admin)
319 explicit, algo, calculate_super_admin)
320 return permissions.calculate()
320 return permissions.calculate()
321
321
322
322
323 class PermOrigin(object):
323 class PermOrigin(object):
324 SUPER_ADMIN = 'superadmin'
324 SUPER_ADMIN = 'superadmin'
325
325
326 REPO_USER = 'user:%s'
326 REPO_USER = 'user:%s'
327 REPO_USERGROUP = 'usergroup:%s'
327 REPO_USERGROUP = 'usergroup:%s'
328 REPO_OWNER = 'repo.owner'
328 REPO_OWNER = 'repo.owner'
329 REPO_DEFAULT = 'repo.default'
329 REPO_DEFAULT = 'repo.default'
330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
331 REPO_PRIVATE = 'repo.private'
331 REPO_PRIVATE = 'repo.private'
332
332
333 REPOGROUP_USER = 'user:%s'
333 REPOGROUP_USER = 'user:%s'
334 REPOGROUP_USERGROUP = 'usergroup:%s'
334 REPOGROUP_USERGROUP = 'usergroup:%s'
335 REPOGROUP_OWNER = 'group.owner'
335 REPOGROUP_OWNER = 'group.owner'
336 REPOGROUP_DEFAULT = 'group.default'
336 REPOGROUP_DEFAULT = 'group.default'
337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
338
338
339 USERGROUP_USER = 'user:%s'
339 USERGROUP_USER = 'user:%s'
340 USERGROUP_USERGROUP = 'usergroup:%s'
340 USERGROUP_USERGROUP = 'usergroup:%s'
341 USERGROUP_OWNER = 'usergroup.owner'
341 USERGROUP_OWNER = 'usergroup.owner'
342 USERGROUP_DEFAULT = 'usergroup.default'
342 USERGROUP_DEFAULT = 'usergroup.default'
343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
344
344
345
345
346 class PermOriginDict(dict):
346 class PermOriginDict(dict):
347 """
347 """
348 A special dict used for tracking permissions along with their origins.
348 A special dict used for tracking permissions along with their origins.
349
349
350 `__setitem__` has been overridden to expect a tuple(perm, origin)
350 `__setitem__` has been overridden to expect a tuple(perm, origin)
351 `__getitem__` will return only the perm
351 `__getitem__` will return only the perm
352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
353
353
354 >>> perms = PermOriginDict()
354 >>> perms = PermOriginDict()
355 >>> perms['resource'] = 'read', 'default'
355 >>> perms['resource'] = 'read', 'default'
356 >>> perms['resource']
356 >>> perms['resource']
357 'read'
357 'read'
358 >>> perms['resource'] = 'write', 'admin'
358 >>> perms['resource'] = 'write', 'admin'
359 >>> perms['resource']
359 >>> perms['resource']
360 'write'
360 'write'
361 >>> perms.perm_origin_stack
361 >>> perms.perm_origin_stack
362 {'resource': [('read', 'default'), ('write', 'admin')]}
362 {'resource': [('read', 'default'), ('write', 'admin')]}
363 """
363 """
364
364
365 def __init__(self, *args, **kw):
365 def __init__(self, *args, **kw):
366 dict.__init__(self, *args, **kw)
366 dict.__init__(self, *args, **kw)
367 self.perm_origin_stack = collections.OrderedDict()
367 self.perm_origin_stack = collections.OrderedDict()
368
368
369 def __setitem__(self, key, (perm, origin)):
369 def __setitem__(self, key, (perm, origin)):
370 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
370 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
371 dict.__setitem__(self, key, perm)
371 dict.__setitem__(self, key, perm)
372
372
373
373
374 class PermissionCalculator(object):
374 class PermissionCalculator(object):
375
375
376 def __init__(
376 def __init__(
377 self, user_id, scope, user_is_admin,
377 self, user_id, scope, user_is_admin,
378 user_inherit_default_permissions, explicit, algo,
378 user_inherit_default_permissions, explicit, algo,
379 calculate_super_admin=False):
379 calculate_super_admin=False):
380
380
381 self.user_id = user_id
381 self.user_id = user_id
382 self.user_is_admin = user_is_admin
382 self.user_is_admin = user_is_admin
383 self.inherit_default_permissions = user_inherit_default_permissions
383 self.inherit_default_permissions = user_inherit_default_permissions
384 self.explicit = explicit
384 self.explicit = explicit
385 self.algo = algo
385 self.algo = algo
386 self.calculate_super_admin = calculate_super_admin
386 self.calculate_super_admin = calculate_super_admin
387
387
388 scope = scope or {}
388 scope = scope or {}
389 self.scope_repo_id = scope.get('repo_id')
389 self.scope_repo_id = scope.get('repo_id')
390 self.scope_repo_group_id = scope.get('repo_group_id')
390 self.scope_repo_group_id = scope.get('repo_group_id')
391 self.scope_user_group_id = scope.get('user_group_id')
391 self.scope_user_group_id = scope.get('user_group_id')
392
392
393 self.default_user_id = User.get_default_user(cache=True).user_id
393 self.default_user_id = User.get_default_user(cache=True).user_id
394
394
395 self.permissions_repositories = PermOriginDict()
395 self.permissions_repositories = PermOriginDict()
396 self.permissions_repository_groups = PermOriginDict()
396 self.permissions_repository_groups = PermOriginDict()
397 self.permissions_user_groups = PermOriginDict()
397 self.permissions_user_groups = PermOriginDict()
398 self.permissions_global = set()
398 self.permissions_global = set()
399
399
400 self.default_repo_perms = Permission.get_default_repo_perms(
400 self.default_repo_perms = Permission.get_default_repo_perms(
401 self.default_user_id, self.scope_repo_id)
401 self.default_user_id, self.scope_repo_id)
402 self.default_repo_groups_perms = Permission.get_default_group_perms(
402 self.default_repo_groups_perms = Permission.get_default_group_perms(
403 self.default_user_id, self.scope_repo_group_id)
403 self.default_user_id, self.scope_repo_group_id)
404 self.default_user_group_perms = \
404 self.default_user_group_perms = \
405 Permission.get_default_user_group_perms(
405 Permission.get_default_user_group_perms(
406 self.default_user_id, self.scope_user_group_id)
406 self.default_user_id, self.scope_user_group_id)
407
407
408 def calculate(self):
408 def calculate(self):
409 if self.user_is_admin and not self.calculate_super_admin:
409 if self.user_is_admin and not self.calculate_super_admin:
410 return self._admin_permissions()
410 return self._admin_permissions()
411
411
412 self._calculate_global_default_permissions()
412 self._calculate_global_default_permissions()
413 self._calculate_global_permissions()
413 self._calculate_global_permissions()
414 self._calculate_default_permissions()
414 self._calculate_default_permissions()
415 self._calculate_repository_permissions()
415 self._calculate_repository_permissions()
416 self._calculate_repository_group_permissions()
416 self._calculate_repository_group_permissions()
417 self._calculate_user_group_permissions()
417 self._calculate_user_group_permissions()
418 return self._permission_structure()
418 return self._permission_structure()
419
419
420 def _admin_permissions(self):
420 def _admin_permissions(self):
421 """
421 """
422 admin user have all default rights for repositories
422 admin user have all default rights for repositories
423 and groups set to admin
423 and groups set to admin
424 """
424 """
425 self.permissions_global.add('hg.admin')
425 self.permissions_global.add('hg.admin')
426 self.permissions_global.add('hg.create.write_on_repogroup.true')
426 self.permissions_global.add('hg.create.write_on_repogroup.true')
427
427
428 # repositories
428 # repositories
429 for perm in self.default_repo_perms:
429 for perm in self.default_repo_perms:
430 r_k = perm.UserRepoToPerm.repository.repo_name
430 r_k = perm.UserRepoToPerm.repository.repo_name
431 p = 'repository.admin'
431 p = 'repository.admin'
432 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
432 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
433
433
434 # repository groups
434 # repository groups
435 for perm in self.default_repo_groups_perms:
435 for perm in self.default_repo_groups_perms:
436 rg_k = perm.UserRepoGroupToPerm.group.group_name
436 rg_k = perm.UserRepoGroupToPerm.group.group_name
437 p = 'group.admin'
437 p = 'group.admin'
438 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN
438 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN
439
439
440 # user groups
440 # user groups
441 for perm in self.default_user_group_perms:
441 for perm in self.default_user_group_perms:
442 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
442 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
443 p = 'usergroup.admin'
443 p = 'usergroup.admin'
444 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN
444 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN
445
445
446 return self._permission_structure()
446 return self._permission_structure()
447
447
448 def _calculate_global_default_permissions(self):
448 def _calculate_global_default_permissions(self):
449 """
449 """
450 global permissions taken from the default user
450 global permissions taken from the default user
451 """
451 """
452 default_global_perms = UserToPerm.query()\
452 default_global_perms = UserToPerm.query()\
453 .filter(UserToPerm.user_id == self.default_user_id)\
453 .filter(UserToPerm.user_id == self.default_user_id)\
454 .options(joinedload(UserToPerm.permission))
454 .options(joinedload(UserToPerm.permission))
455
455
456 for perm in default_global_perms:
456 for perm in default_global_perms:
457 self.permissions_global.add(perm.permission.permission_name)
457 self.permissions_global.add(perm.permission.permission_name)
458
458
459 if self.user_is_admin:
459 if self.user_is_admin:
460 self.permissions_global.add('hg.admin')
460 self.permissions_global.add('hg.admin')
461 self.permissions_global.add('hg.create.write_on_repogroup.true')
461 self.permissions_global.add('hg.create.write_on_repogroup.true')
462
462
463 def _calculate_global_permissions(self):
463 def _calculate_global_permissions(self):
464 """
464 """
465 Set global system permissions with user permissions or permissions
465 Set global system permissions with user permissions or permissions
466 taken from the user groups of the current user.
466 taken from the user groups of the current user.
467
467
468 The permissions include repo creating, repo group creating, forking
468 The permissions include repo creating, repo group creating, forking
469 etc.
469 etc.
470 """
470 """
471
471
472 # now we read the defined permissions and overwrite what we have set
472 # now we read the defined permissions and overwrite what we have set
473 # before those can be configured from groups or users explicitly.
473 # before those can be configured from groups or users explicitly.
474
474
475 # TODO: johbo: This seems to be out of sync, find out the reason
475 # TODO: johbo: This seems to be out of sync, find out the reason
476 # for the comment below and update it.
476 # for the comment below and update it.
477
477
478 # In case we want to extend this list we should be always in sync with
478 # In case we want to extend this list we should be always in sync with
479 # User.DEFAULT_USER_PERMISSIONS definitions
479 # User.DEFAULT_USER_PERMISSIONS definitions
480 _configurable = frozenset([
480 _configurable = frozenset([
481 'hg.fork.none', 'hg.fork.repository',
481 'hg.fork.none', 'hg.fork.repository',
482 'hg.create.none', 'hg.create.repository',
482 'hg.create.none', 'hg.create.repository',
483 'hg.usergroup.create.false', 'hg.usergroup.create.true',
483 'hg.usergroup.create.false', 'hg.usergroup.create.true',
484 'hg.repogroup.create.false', 'hg.repogroup.create.true',
484 'hg.repogroup.create.false', 'hg.repogroup.create.true',
485 'hg.create.write_on_repogroup.false',
485 'hg.create.write_on_repogroup.false',
486 'hg.create.write_on_repogroup.true',
486 'hg.create.write_on_repogroup.true',
487 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
487 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
488 ])
488 ])
489
489
490 # USER GROUPS comes first user group global permissions
490 # USER GROUPS comes first user group global permissions
491 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
491 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
492 .options(joinedload(UserGroupToPerm.permission))\
492 .options(joinedload(UserGroupToPerm.permission))\
493 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
493 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
494 UserGroupMember.users_group_id))\
494 UserGroupMember.users_group_id))\
495 .filter(UserGroupMember.user_id == self.user_id)\
495 .filter(UserGroupMember.user_id == self.user_id)\
496 .order_by(UserGroupToPerm.users_group_id)\
496 .order_by(UserGroupToPerm.users_group_id)\
497 .all()
497 .all()
498
498
499 # need to group here by groups since user can be in more than
499 # need to group here by groups since user can be in more than
500 # one group, so we get all groups
500 # one group, so we get all groups
501 _explicit_grouped_perms = [
501 _explicit_grouped_perms = [
502 [x, list(y)] for x, y in
502 [x, list(y)] for x, y in
503 itertools.groupby(user_perms_from_users_groups,
503 itertools.groupby(user_perms_from_users_groups,
504 lambda _x: _x.users_group)]
504 lambda _x: _x.users_group)]
505
505
506 for gr, perms in _explicit_grouped_perms:
506 for gr, perms in _explicit_grouped_perms:
507 # since user can be in multiple groups iterate over them and
507 # since user can be in multiple groups iterate over them and
508 # select the lowest permissions first (more explicit)
508 # select the lowest permissions first (more explicit)
509 # TODO: marcink: do this^^
509 # TODO: marcink: do this^^
510
510
511 # group doesn't inherit default permissions so we actually set them
511 # group doesn't inherit default permissions so we actually set them
512 if not gr.inherit_default_permissions:
512 if not gr.inherit_default_permissions:
513 # NEED TO IGNORE all previously set configurable permissions
513 # NEED TO IGNORE all previously set configurable permissions
514 # and replace them with explicitly set from this user
514 # and replace them with explicitly set from this user
515 # group permissions
515 # group permissions
516 self.permissions_global = self.permissions_global.difference(
516 self.permissions_global = self.permissions_global.difference(
517 _configurable)
517 _configurable)
518 for perm in perms:
518 for perm in perms:
519 self.permissions_global.add(perm.permission.permission_name)
519 self.permissions_global.add(perm.permission.permission_name)
520
520
521 # user explicit global permissions
521 # user explicit global permissions
522 user_perms = Session().query(UserToPerm)\
522 user_perms = Session().query(UserToPerm)\
523 .options(joinedload(UserToPerm.permission))\
523 .options(joinedload(UserToPerm.permission))\
524 .filter(UserToPerm.user_id == self.user_id).all()
524 .filter(UserToPerm.user_id == self.user_id).all()
525
525
526 if not self.inherit_default_permissions:
526 if not self.inherit_default_permissions:
527 # NEED TO IGNORE all configurable permissions and
527 # NEED TO IGNORE all configurable permissions and
528 # replace them with explicitly set from this user permissions
528 # replace them with explicitly set from this user permissions
529 self.permissions_global = self.permissions_global.difference(
529 self.permissions_global = self.permissions_global.difference(
530 _configurable)
530 _configurable)
531 for perm in user_perms:
531 for perm in user_perms:
532 self.permissions_global.add(perm.permission.permission_name)
532 self.permissions_global.add(perm.permission.permission_name)
533
533
534 def _calculate_default_permissions(self):
534 def _calculate_default_permissions(self):
535 """
535 """
536 Set default user permissions for repositories, repository groups
536 Set default user permissions for repositories, repository groups
537 taken from the default user.
537 taken from the default user.
538
538
539 Calculate inheritance of object permissions based on what we have now
539 Calculate inheritance of object permissions based on what we have now
540 in GLOBAL permissions. We check if .false is in GLOBAL since this is
540 in GLOBAL permissions. We check if .false is in GLOBAL since this is
541 explicitly set. Inherit is the opposite of .false being there.
541 explicitly set. Inherit is the opposite of .false being there.
542
542
543 .. note::
543 .. note::
544
544
545 the syntax is little bit odd but what we need to check here is
545 the syntax is little bit odd but what we need to check here is
546 the opposite of .false permission being in the list so even for
546 the opposite of .false permission being in the list so even for
547 inconsistent state when both .true/.false is there
547 inconsistent state when both .true/.false is there
548 .false is more important
548 .false is more important
549
549
550 """
550 """
551 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
551 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
552 in self.permissions_global)
552 in self.permissions_global)
553
553
554 # defaults for repositories, taken from `default` user permissions
554 # defaults for repositories, taken from `default` user permissions
555 # on given repo
555 # on given repo
556 for perm in self.default_repo_perms:
556 for perm in self.default_repo_perms:
557 r_k = perm.UserRepoToPerm.repository.repo_name
557 r_k = perm.UserRepoToPerm.repository.repo_name
558 p = perm.Permission.permission_name
558 p = perm.Permission.permission_name
559 o = PermOrigin.REPO_DEFAULT
559 o = PermOrigin.REPO_DEFAULT
560 self.permissions_repositories[r_k] = p, o
560 self.permissions_repositories[r_k] = p, o
561
561
562 # if we decide this user isn't inheriting permissions from
562 # if we decide this user isn't inheriting permissions from
563 # default user we set him to .none so only explicit
563 # default user we set him to .none so only explicit
564 # permissions work
564 # permissions work
565 if not user_inherit_object_permissions:
565 if not user_inherit_object_permissions:
566 p = 'repository.none'
566 p = 'repository.none'
567 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
567 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
568 self.permissions_repositories[r_k] = p, o
568 self.permissions_repositories[r_k] = p, o
569
569
570 if perm.Repository.private and not (
570 if perm.Repository.private and not (
571 perm.Repository.user_id == self.user_id):
571 perm.Repository.user_id == self.user_id):
572 # disable defaults for private repos,
572 # disable defaults for private repos,
573 p = 'repository.none'
573 p = 'repository.none'
574 o = PermOrigin.REPO_PRIVATE
574 o = PermOrigin.REPO_PRIVATE
575 self.permissions_repositories[r_k] = p, o
575 self.permissions_repositories[r_k] = p, o
576
576
577 elif perm.Repository.user_id == self.user_id:
577 elif perm.Repository.user_id == self.user_id:
578 # set admin if owner
578 # set admin if owner
579 p = 'repository.admin'
579 p = 'repository.admin'
580 o = PermOrigin.REPO_OWNER
580 o = PermOrigin.REPO_OWNER
581 self.permissions_repositories[r_k] = p, o
581 self.permissions_repositories[r_k] = p, o
582
582
583 if self.user_is_admin:
583 if self.user_is_admin:
584 p = 'repository.admin'
584 p = 'repository.admin'
585 o = PermOrigin.SUPER_ADMIN
585 o = PermOrigin.SUPER_ADMIN
586 self.permissions_repositories[r_k] = p, o
586 self.permissions_repositories[r_k] = p, o
587
587
588 # defaults for repository groups taken from `default` user permission
588 # defaults for repository groups taken from `default` user permission
589 # on given group
589 # on given group
590 for perm in self.default_repo_groups_perms:
590 for perm in self.default_repo_groups_perms:
591 rg_k = perm.UserRepoGroupToPerm.group.group_name
591 rg_k = perm.UserRepoGroupToPerm.group.group_name
592 p = perm.Permission.permission_name
592 p = perm.Permission.permission_name
593 o = PermOrigin.REPOGROUP_DEFAULT
593 o = PermOrigin.REPOGROUP_DEFAULT
594 self.permissions_repository_groups[rg_k] = p, o
594 self.permissions_repository_groups[rg_k] = p, o
595
595
596 # if we decide this user isn't inheriting permissions from default
596 # if we decide this user isn't inheriting permissions from default
597 # user we set him to .none so only explicit permissions work
597 # user we set him to .none so only explicit permissions work
598 if not user_inherit_object_permissions:
598 if not user_inherit_object_permissions:
599 p = 'group.none'
599 p = 'group.none'
600 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
600 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
601 self.permissions_repository_groups[rg_k] = p, o
601 self.permissions_repository_groups[rg_k] = p, o
602
602
603 if perm.RepoGroup.user_id == self.user_id:
603 if perm.RepoGroup.user_id == self.user_id:
604 # set admin if owner
604 # set admin if owner
605 p = 'group.admin'
605 p = 'group.admin'
606 o = PermOrigin.REPOGROUP_OWNER
606 o = PermOrigin.REPOGROUP_OWNER
607 self.permissions_repository_groups[rg_k] = p, o
607 self.permissions_repository_groups[rg_k] = p, o
608
608
609 if self.user_is_admin:
609 if self.user_is_admin:
610 p = 'group.admin'
610 p = 'group.admin'
611 o = PermOrigin.SUPER_ADMIN
611 o = PermOrigin.SUPER_ADMIN
612 self.permissions_repository_groups[rg_k] = p, o
612 self.permissions_repository_groups[rg_k] = p, o
613
613
614 # defaults for user groups taken from `default` user permission
614 # defaults for user groups taken from `default` user permission
615 # on given user group
615 # on given user group
616 for perm in self.default_user_group_perms:
616 for perm in self.default_user_group_perms:
617 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
617 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
618 p = perm.Permission.permission_name
618 p = perm.Permission.permission_name
619 o = PermOrigin.USERGROUP_DEFAULT
619 o = PermOrigin.USERGROUP_DEFAULT
620 self.permissions_user_groups[u_k] = p, o
620 self.permissions_user_groups[u_k] = p, o
621
621
622 # if we decide this user isn't inheriting permissions from default
622 # if we decide this user isn't inheriting permissions from default
623 # user we set him to .none so only explicit permissions work
623 # user we set him to .none so only explicit permissions work
624 if not user_inherit_object_permissions:
624 if not user_inherit_object_permissions:
625 p = 'usergroup.none'
625 p = 'usergroup.none'
626 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
626 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
627 self.permissions_user_groups[u_k] = p, o
627 self.permissions_user_groups[u_k] = p, o
628
628
629 if perm.UserGroup.user_id == self.user_id:
629 if perm.UserGroup.user_id == self.user_id:
630 # set admin if owner
630 # set admin if owner
631 p = 'usergroup.admin'
631 p = 'usergroup.admin'
632 o = PermOrigin.USERGROUP_OWNER
632 o = PermOrigin.USERGROUP_OWNER
633 self.permissions_user_groups[u_k] = p, o
633 self.permissions_user_groups[u_k] = p, o
634
634
635 if self.user_is_admin:
635 if self.user_is_admin:
636 p = 'usergroup.admin'
636 p = 'usergroup.admin'
637 o = PermOrigin.SUPER_ADMIN
637 o = PermOrigin.SUPER_ADMIN
638 self.permissions_user_groups[u_k] = p, o
638 self.permissions_user_groups[u_k] = p, o
639
639
640 def _calculate_repository_permissions(self):
640 def _calculate_repository_permissions(self):
641 """
641 """
642 Repository permissions for the current user.
642 Repository permissions for the current user.
643
643
644 Check if the user is part of user groups for this repository and
644 Check if the user is part of user groups for this repository and
645 fill in the permission from it. `_choose_permission` decides of which
645 fill in the permission from it. `_choose_permission` decides of which
646 permission should be selected based on selected method.
646 permission should be selected based on selected method.
647 """
647 """
648
648
649 # user group for repositories permissions
649 # user group for repositories permissions
650 user_repo_perms_from_user_group = Permission\
650 user_repo_perms_from_user_group = Permission\
651 .get_default_repo_perms_from_user_group(
651 .get_default_repo_perms_from_user_group(
652 self.user_id, self.scope_repo_id)
652 self.user_id, self.scope_repo_id)
653
653
654 multiple_counter = collections.defaultdict(int)
654 multiple_counter = collections.defaultdict(int)
655 for perm in user_repo_perms_from_user_group:
655 for perm in user_repo_perms_from_user_group:
656 r_k = perm.UserGroupRepoToPerm.repository.repo_name
656 r_k = perm.UserGroupRepoToPerm.repository.repo_name
657 multiple_counter[r_k] += 1
657 multiple_counter[r_k] += 1
658 p = perm.Permission.permission_name
658 p = perm.Permission.permission_name
659 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
659 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
660 .users_group.users_group_name
660 .users_group.users_group_name
661
661
662 if multiple_counter[r_k] > 1:
662 if multiple_counter[r_k] > 1:
663 cur_perm = self.permissions_repositories[r_k]
663 cur_perm = self.permissions_repositories[r_k]
664 p = self._choose_permission(p, cur_perm)
664 p = self._choose_permission(p, cur_perm)
665
665
666 self.permissions_repositories[r_k] = p, o
666 self.permissions_repositories[r_k] = p, o
667
667
668 if perm.Repository.user_id == self.user_id:
668 if perm.Repository.user_id == self.user_id:
669 # set admin if owner
669 # set admin if owner
670 p = 'repository.admin'
670 p = 'repository.admin'
671 o = PermOrigin.REPO_OWNER
671 o = PermOrigin.REPO_OWNER
672 self.permissions_repositories[r_k] = p, o
672 self.permissions_repositories[r_k] = p, o
673
673
674 if self.user_is_admin:
674 if self.user_is_admin:
675 p = 'repository.admin'
675 p = 'repository.admin'
676 o = PermOrigin.SUPER_ADMIN
676 o = PermOrigin.SUPER_ADMIN
677 self.permissions_repositories[r_k] = p, o
677 self.permissions_repositories[r_k] = p, o
678
678
679 # user explicit permissions for repositories, overrides any specified
679 # user explicit permissions for repositories, overrides any specified
680 # by the group permission
680 # by the group permission
681 user_repo_perms = Permission.get_default_repo_perms(
681 user_repo_perms = Permission.get_default_repo_perms(
682 self.user_id, self.scope_repo_id)
682 self.user_id, self.scope_repo_id)
683 for perm in user_repo_perms:
683 for perm in user_repo_perms:
684 r_k = perm.UserRepoToPerm.repository.repo_name
684 r_k = perm.UserRepoToPerm.repository.repo_name
685 p = perm.Permission.permission_name
685 p = perm.Permission.permission_name
686 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
686 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
687
687
688 if not self.explicit:
688 if not self.explicit:
689 cur_perm = self.permissions_repositories.get(
689 cur_perm = self.permissions_repositories.get(
690 r_k, 'repository.none')
690 r_k, 'repository.none')
691 p = self._choose_permission(p, cur_perm)
691 p = self._choose_permission(p, cur_perm)
692
692
693 self.permissions_repositories[r_k] = p, o
693 self.permissions_repositories[r_k] = p, o
694
694
695 if perm.Repository.user_id == self.user_id:
695 if perm.Repository.user_id == self.user_id:
696 # set admin if owner
696 # set admin if owner
697 p = 'repository.admin'
697 p = 'repository.admin'
698 o = PermOrigin.REPO_OWNER
698 o = PermOrigin.REPO_OWNER
699 self.permissions_repositories[r_k] = p, o
699 self.permissions_repositories[r_k] = p, o
700
700
701 if self.user_is_admin:
701 if self.user_is_admin:
702 p = 'repository.admin'
702 p = 'repository.admin'
703 o = PermOrigin.SUPER_ADMIN
703 o = PermOrigin.SUPER_ADMIN
704 self.permissions_repositories[r_k] = p, o
704 self.permissions_repositories[r_k] = p, o
705
705
706 def _calculate_repository_group_permissions(self):
706 def _calculate_repository_group_permissions(self):
707 """
707 """
708 Repository group permissions for the current user.
708 Repository group permissions for the current user.
709
709
710 Check if the user is part of user groups for repository groups and
710 Check if the user is part of user groups for repository groups and
711 fill in the permissions from it. `_choose_permission` decides of which
711 fill in the permissions from it. `_choose_permission` decides of which
712 permission should be selected based on selected method.
712 permission should be selected based on selected method.
713 """
713 """
714 # user group for repo groups permissions
714 # user group for repo groups permissions
715 user_repo_group_perms_from_user_group = Permission\
715 user_repo_group_perms_from_user_group = Permission\
716 .get_default_group_perms_from_user_group(
716 .get_default_group_perms_from_user_group(
717 self.user_id, self.scope_repo_group_id)
717 self.user_id, self.scope_repo_group_id)
718
718
719 multiple_counter = collections.defaultdict(int)
719 multiple_counter = collections.defaultdict(int)
720 for perm in user_repo_group_perms_from_user_group:
720 for perm in user_repo_group_perms_from_user_group:
721 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
721 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
722 multiple_counter[rg_k] += 1
722 multiple_counter[rg_k] += 1
723 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
723 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
724 .users_group.users_group_name
724 .users_group.users_group_name
725 p = perm.Permission.permission_name
725 p = perm.Permission.permission_name
726
726
727 if multiple_counter[rg_k] > 1:
727 if multiple_counter[rg_k] > 1:
728 cur_perm = self.permissions_repository_groups[rg_k]
728 cur_perm = self.permissions_repository_groups[rg_k]
729 p = self._choose_permission(p, cur_perm)
729 p = self._choose_permission(p, cur_perm)
730 self.permissions_repository_groups[rg_k] = p, o
730 self.permissions_repository_groups[rg_k] = p, o
731
731
732 if perm.RepoGroup.user_id == self.user_id:
732 if perm.RepoGroup.user_id == self.user_id:
733 # set admin if owner, even for member of other user group
733 # set admin if owner, even for member of other user group
734 p = 'group.admin'
734 p = 'group.admin'
735 o = PermOrigin.REPOGROUP_OWNER
735 o = PermOrigin.REPOGROUP_OWNER
736 self.permissions_repository_groups[rg_k] = p, o
736 self.permissions_repository_groups[rg_k] = p, o
737
737
738 if self.user_is_admin:
738 if self.user_is_admin:
739 p = 'group.admin'
739 p = 'group.admin'
740 o = PermOrigin.SUPER_ADMIN
740 o = PermOrigin.SUPER_ADMIN
741 self.permissions_repository_groups[rg_k] = p, o
741 self.permissions_repository_groups[rg_k] = p, o
742
742
743 # user explicit permissions for repository groups
743 # user explicit permissions for repository groups
744 user_repo_groups_perms = Permission.get_default_group_perms(
744 user_repo_groups_perms = Permission.get_default_group_perms(
745 self.user_id, self.scope_repo_group_id)
745 self.user_id, self.scope_repo_group_id)
746 for perm in user_repo_groups_perms:
746 for perm in user_repo_groups_perms:
747 rg_k = perm.UserRepoGroupToPerm.group.group_name
747 rg_k = perm.UserRepoGroupToPerm.group.group_name
748 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
748 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
749 .user.username
749 .user.username
750 p = perm.Permission.permission_name
750 p = perm.Permission.permission_name
751
751
752 if not self.explicit:
752 if not self.explicit:
753 cur_perm = self.permissions_repository_groups.get(
753 cur_perm = self.permissions_repository_groups.get(
754 rg_k, 'group.none')
754 rg_k, 'group.none')
755 p = self._choose_permission(p, cur_perm)
755 p = self._choose_permission(p, cur_perm)
756
756
757 self.permissions_repository_groups[rg_k] = p, o
757 self.permissions_repository_groups[rg_k] = p, o
758
758
759 if perm.RepoGroup.user_id == self.user_id:
759 if perm.RepoGroup.user_id == self.user_id:
760 # set admin if owner
760 # set admin if owner
761 p = 'group.admin'
761 p = 'group.admin'
762 o = PermOrigin.REPOGROUP_OWNER
762 o = PermOrigin.REPOGROUP_OWNER
763 self.permissions_repository_groups[rg_k] = p, o
763 self.permissions_repository_groups[rg_k] = p, o
764
764
765 if self.user_is_admin:
765 if self.user_is_admin:
766 p = 'group.admin'
766 p = 'group.admin'
767 o = PermOrigin.SUPER_ADMIN
767 o = PermOrigin.SUPER_ADMIN
768 self.permissions_repository_groups[rg_k] = p, o
768 self.permissions_repository_groups[rg_k] = p, o
769
769
770 def _calculate_user_group_permissions(self):
770 def _calculate_user_group_permissions(self):
771 """
771 """
772 User group permissions for the current user.
772 User group permissions for the current user.
773 """
773 """
774 # user group for user group permissions
774 # user group for user group permissions
775 user_group_from_user_group = Permission\
775 user_group_from_user_group = Permission\
776 .get_default_user_group_perms_from_user_group(
776 .get_default_user_group_perms_from_user_group(
777 self.user_id, self.scope_user_group_id)
777 self.user_id, self.scope_user_group_id)
778
778
779 multiple_counter = collections.defaultdict(int)
779 multiple_counter = collections.defaultdict(int)
780 for perm in user_group_from_user_group:
780 for perm in user_group_from_user_group:
781 ug_k = perm.UserGroupUserGroupToPerm\
781 ug_k = perm.UserGroupUserGroupToPerm\
782 .target_user_group.users_group_name
782 .target_user_group.users_group_name
783 multiple_counter[ug_k] += 1
783 multiple_counter[ug_k] += 1
784 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
784 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
785 .user_group.users_group_name
785 .user_group.users_group_name
786 p = perm.Permission.permission_name
786 p = perm.Permission.permission_name
787
787
788 if multiple_counter[ug_k] > 1:
788 if multiple_counter[ug_k] > 1:
789 cur_perm = self.permissions_user_groups[ug_k]
789 cur_perm = self.permissions_user_groups[ug_k]
790 p = self._choose_permission(p, cur_perm)
790 p = self._choose_permission(p, cur_perm)
791
791
792 self.permissions_user_groups[ug_k] = p, o
792 self.permissions_user_groups[ug_k] = p, o
793
793
794 if perm.UserGroup.user_id == self.user_id:
794 if perm.UserGroup.user_id == self.user_id:
795 # set admin if owner, even for member of other user group
795 # set admin if owner, even for member of other user group
796 p = 'usergroup.admin'
796 p = 'usergroup.admin'
797 o = PermOrigin.USERGROUP_OWNER
797 o = PermOrigin.USERGROUP_OWNER
798 self.permissions_user_groups[ug_k] = p, o
798 self.permissions_user_groups[ug_k] = p, o
799
799
800 if self.user_is_admin:
800 if self.user_is_admin:
801 p = 'usergroup.admin'
801 p = 'usergroup.admin'
802 o = PermOrigin.SUPER_ADMIN
802 o = PermOrigin.SUPER_ADMIN
803 self.permissions_user_groups[ug_k] = p, o
803 self.permissions_user_groups[ug_k] = p, o
804
804
805 # user explicit permission for user groups
805 # user explicit permission for user groups
806 user_user_groups_perms = Permission.get_default_user_group_perms(
806 user_user_groups_perms = Permission.get_default_user_group_perms(
807 self.user_id, self.scope_user_group_id)
807 self.user_id, self.scope_user_group_id)
808 for perm in user_user_groups_perms:
808 for perm in user_user_groups_perms:
809 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
809 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
810 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
810 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
811 .user.username
811 .user.username
812 p = perm.Permission.permission_name
812 p = perm.Permission.permission_name
813
813
814 if not self.explicit:
814 if not self.explicit:
815 cur_perm = self.permissions_user_groups.get(
815 cur_perm = self.permissions_user_groups.get(
816 ug_k, 'usergroup.none')
816 ug_k, 'usergroup.none')
817 p = self._choose_permission(p, cur_perm)
817 p = self._choose_permission(p, cur_perm)
818
818
819 self.permissions_user_groups[ug_k] = p, o
819 self.permissions_user_groups[ug_k] = p, o
820
820
821 if perm.UserGroup.user_id == self.user_id:
821 if perm.UserGroup.user_id == self.user_id:
822 # set admin if owner
822 # set admin if owner
823 p = 'usergroup.admin'
823 p = 'usergroup.admin'
824 o = PermOrigin.USERGROUP_OWNER
824 o = PermOrigin.USERGROUP_OWNER
825 self.permissions_user_groups[ug_k] = p, o
825 self.permissions_user_groups[ug_k] = p, o
826
826
827 if self.user_is_admin:
827 if self.user_is_admin:
828 p = 'usergroup.admin'
828 p = 'usergroup.admin'
829 o = PermOrigin.SUPER_ADMIN
829 o = PermOrigin.SUPER_ADMIN
830 self.permissions_user_groups[ug_k] = p, o
830 self.permissions_user_groups[ug_k] = p, o
831
831
832 def _choose_permission(self, new_perm, cur_perm):
832 def _choose_permission(self, new_perm, cur_perm):
833 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
833 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
834 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
834 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
835 if self.algo == 'higherwin':
835 if self.algo == 'higherwin':
836 if new_perm_val > cur_perm_val:
836 if new_perm_val > cur_perm_val:
837 return new_perm
837 return new_perm
838 return cur_perm
838 return cur_perm
839 elif self.algo == 'lowerwin':
839 elif self.algo == 'lowerwin':
840 if new_perm_val < cur_perm_val:
840 if new_perm_val < cur_perm_val:
841 return new_perm
841 return new_perm
842 return cur_perm
842 return cur_perm
843
843
844 def _permission_structure(self):
844 def _permission_structure(self):
845 return {
845 return {
846 'global': self.permissions_global,
846 'global': self.permissions_global,
847 'repositories': self.permissions_repositories,
847 'repositories': self.permissions_repositories,
848 'repositories_groups': self.permissions_repository_groups,
848 'repositories_groups': self.permissions_repository_groups,
849 'user_groups': self.permissions_user_groups,
849 'user_groups': self.permissions_user_groups,
850 }
850 }
851
851
852
852
853 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
853 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
854 """
854 """
855 Check if given controller_name is in whitelist of auth token access
855 Check if given controller_name is in whitelist of auth token access
856 """
856 """
857 if not whitelist:
857 if not whitelist:
858 from rhodecode import CONFIG
858 from rhodecode import CONFIG
859 whitelist = aslist(
859 whitelist = aslist(
860 CONFIG.get('api_access_controllers_whitelist'), sep=',')
860 CONFIG.get('api_access_controllers_whitelist'), sep=',')
861 # backward compat translation
861 # backward compat translation
862 compat = {
862 compat = {
863 # old controller, new VIEW
863 # old controller, new VIEW
864 'ChangesetController:*': 'RepoCommitsView:*',
864 'ChangesetController:*': 'RepoCommitsView:*',
865 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
865 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
866 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
866 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
867 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
867 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
868 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
868 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
869 'GistsController:*': 'GistView:*',
869 'GistsController:*': 'GistView:*',
870 }
870 }
871
871
872 log.debug(
872 log.debug(
873 'Allowed views for AUTH TOKEN access: %s' % (whitelist,))
873 'Allowed views for AUTH TOKEN access: %s' % (whitelist,))
874 auth_token_access_valid = False
874 auth_token_access_valid = False
875
875
876 for entry in whitelist:
876 for entry in whitelist:
877 token_match = True
877 token_match = True
878 if entry in compat:
878 if entry in compat:
879 # translate from old Controllers to Pyramid Views
879 # translate from old Controllers to Pyramid Views
880 entry = compat[entry]
880 entry = compat[entry]
881
881
882 if '@' in entry:
882 if '@' in entry:
883 # specific AuthToken
883 # specific AuthToken
884 entry, allowed_token = entry.split('@', 1)
884 entry, allowed_token = entry.split('@', 1)
885 token_match = auth_token == allowed_token
885 token_match = auth_token == allowed_token
886
886
887 if fnmatch.fnmatch(view_name, entry) and token_match:
887 if fnmatch.fnmatch(view_name, entry) and token_match:
888 auth_token_access_valid = True
888 auth_token_access_valid = True
889 break
889 break
890
890
891 if auth_token_access_valid:
891 if auth_token_access_valid:
892 log.debug('view: `%s` matches entry in whitelist: %s'
892 log.debug('view: `%s` matches entry in whitelist: %s'
893 % (view_name, whitelist))
893 % (view_name, whitelist))
894 else:
894 else:
895 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
895 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
896 % (view_name, whitelist))
896 % (view_name, whitelist))
897 if auth_token:
897 if auth_token:
898 # if we use auth token key and don't have access it's a warning
898 # if we use auth token key and don't have access it's a warning
899 log.warning(msg)
899 log.warning(msg)
900 else:
900 else:
901 log.debug(msg)
901 log.debug(msg)
902
902
903 return auth_token_access_valid
903 return auth_token_access_valid
904
904
905
905
906 class AuthUser(object):
906 class AuthUser(object):
907 """
907 """
908 A simple object that handles all attributes of user in RhodeCode
908 A simple object that handles all attributes of user in RhodeCode
909
909
910 It does lookup based on API key,given user, or user present in session
910 It does lookup based on API key,given user, or user present in session
911 Then it fills all required information for such user. It also checks if
911 Then it fills all required information for such user. It also checks if
912 anonymous access is enabled and if so, it returns default user as logged in
912 anonymous access is enabled and if so, it returns default user as logged in
913 """
913 """
914 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
914 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
915
915
916 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
916 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
917
917
918 self.user_id = user_id
918 self.user_id = user_id
919 self._api_key = api_key
919 self._api_key = api_key
920
920
921 self.api_key = None
921 self.api_key = None
922 self.username = username
922 self.username = username
923 self.ip_addr = ip_addr
923 self.ip_addr = ip_addr
924 self.name = ''
924 self.name = ''
925 self.lastname = ''
925 self.lastname = ''
926 self.first_name = ''
926 self.first_name = ''
927 self.last_name = ''
927 self.last_name = ''
928 self.email = ''
928 self.email = ''
929 self.is_authenticated = False
929 self.is_authenticated = False
930 self.admin = False
930 self.admin = False
931 self.inherit_default_permissions = False
931 self.inherit_default_permissions = False
932 self.password = ''
932 self.password = ''
933
933
934 self.anonymous_user = None # propagated on propagate_data
934 self.anonymous_user = None # propagated on propagate_data
935 self.propagate_data()
935 self.propagate_data()
936 self._instance = None
936 self._instance = None
937 self._permissions_scoped_cache = {} # used to bind scoped calculation
937 self._permissions_scoped_cache = {} # used to bind scoped calculation
938
938
939 @LazyProperty
939 @LazyProperty
940 def permissions(self):
940 def permissions(self):
941 return self.get_perms(user=self, cache=False)
941 return self.get_perms(user=self, cache=False)
942
942
943 @LazyProperty
943 @LazyProperty
944 def permissions_safe(self):
944 def permissions_safe(self):
945 """
945 """
946 Filtered permissions excluding not allowed repositories
946 Filtered permissions excluding not allowed repositories
947 """
947 """
948 perms = self.get_perms(user=self, cache=False)
948 perms = self.get_perms(user=self, cache=False)
949
949
950 perms['repositories'] = {
950 perms['repositories'] = {
951 k: v for k, v in perms['repositories'].items()
951 k: v for k, v in perms['repositories'].items()
952 if v != 'repository.none'}
952 if v != 'repository.none'}
953 perms['repositories_groups'] = {
953 perms['repositories_groups'] = {
954 k: v for k, v in perms['repositories_groups'].items()
954 k: v for k, v in perms['repositories_groups'].items()
955 if v != 'group.none'}
955 if v != 'group.none'}
956 perms['user_groups'] = {
956 perms['user_groups'] = {
957 k: v for k, v in perms['user_groups'].items()
957 k: v for k, v in perms['user_groups'].items()
958 if v != 'usergroup.none'}
958 if v != 'usergroup.none'}
959 return perms
959 return perms
960
960
961 @LazyProperty
961 @LazyProperty
962 def permissions_full_details(self):
962 def permissions_full_details(self):
963 return self.get_perms(
963 return self.get_perms(
964 user=self, cache=False, calculate_super_admin=True)
964 user=self, cache=False, calculate_super_admin=True)
965
965
966 def permissions_with_scope(self, scope):
966 def permissions_with_scope(self, scope):
967 """
967 """
968 Call the get_perms function with scoped data. The scope in that function
968 Call the get_perms function with scoped data. The scope in that function
969 narrows the SQL calls to the given ID of objects resulting in fetching
969 narrows the SQL calls to the given ID of objects resulting in fetching
970 Just particular permission we want to obtain. If scope is an empty dict
970 Just particular permission we want to obtain. If scope is an empty dict
971 then it basically narrows the scope to GLOBAL permissions only.
971 then it basically narrows the scope to GLOBAL permissions only.
972
972
973 :param scope: dict
973 :param scope: dict
974 """
974 """
975 if 'repo_name' in scope:
975 if 'repo_name' in scope:
976 obj = Repository.get_by_repo_name(scope['repo_name'])
976 obj = Repository.get_by_repo_name(scope['repo_name'])
977 if obj:
977 if obj:
978 scope['repo_id'] = obj.repo_id
978 scope['repo_id'] = obj.repo_id
979 _scope = collections.OrderedDict()
979 _scope = collections.OrderedDict()
980 _scope['repo_id'] = -1
980 _scope['repo_id'] = -1
981 _scope['user_group_id'] = -1
981 _scope['user_group_id'] = -1
982 _scope['repo_group_id'] = -1
982 _scope['repo_group_id'] = -1
983
983
984 for k in sorted(scope.keys()):
984 for k in sorted(scope.keys()):
985 _scope[k] = scope[k]
985 _scope[k] = scope[k]
986
986
987 # store in cache to mimic how the @LazyProperty works,
987 # store in cache to mimic how the @LazyProperty works,
988 # the difference here is that we use the unique key calculated
988 # the difference here is that we use the unique key calculated
989 # from params and values
989 # from params and values
990 return self.get_perms(user=self, cache=False, scope=_scope)
990 return self.get_perms(user=self, cache=False, scope=_scope)
991
991
992 def get_instance(self):
992 def get_instance(self):
993 return User.get(self.user_id)
993 return User.get(self.user_id)
994
994
995 def update_lastactivity(self):
995 def update_lastactivity(self):
996 if self.user_id:
996 if self.user_id:
997 User.get(self.user_id).update_lastactivity()
997 User.get(self.user_id).update_lastactivity()
998
998
999 def propagate_data(self):
999 def propagate_data(self):
1000 """
1000 """
1001 Fills in user data and propagates values to this instance. Maps fetched
1001 Fills in user data and propagates values to this instance. Maps fetched
1002 user attributes to this class instance attributes
1002 user attributes to this class instance attributes
1003 """
1003 """
1004 log.debug('AuthUser: starting data propagation for new potential user')
1004 log.debug('AuthUser: starting data propagation for new potential user')
1005 user_model = UserModel()
1005 user_model = UserModel()
1006 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1006 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1007 is_user_loaded = False
1007 is_user_loaded = False
1008
1008
1009 # lookup by userid
1009 # lookup by userid
1010 if self.user_id is not None and self.user_id != anon_user.user_id:
1010 if self.user_id is not None and self.user_id != anon_user.user_id:
1011 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1011 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1012 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1012 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1013
1013
1014 # try go get user by api key
1014 # try go get user by api key
1015 elif self._api_key and self._api_key != anon_user.api_key:
1015 elif self._api_key and self._api_key != anon_user.api_key:
1016 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1016 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1017 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1017 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1018
1018
1019 # lookup by username
1019 # lookup by username
1020 elif self.username:
1020 elif self.username:
1021 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1021 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1022 is_user_loaded = user_model.fill_data(self, username=self.username)
1022 is_user_loaded = user_model.fill_data(self, username=self.username)
1023 else:
1023 else:
1024 log.debug('No data in %s that could been used to log in', self)
1024 log.debug('No data in %s that could been used to log in', self)
1025
1025
1026 if not is_user_loaded:
1026 if not is_user_loaded:
1027 log.debug(
1027 log.debug(
1028 'Failed to load user. Fallback to default user %s', anon_user)
1028 'Failed to load user. Fallback to default user %s', anon_user)
1029 # if we cannot authenticate user try anonymous
1029 # if we cannot authenticate user try anonymous
1030 if anon_user.active:
1030 if anon_user.active:
1031 log.debug('default user is active, using it as a session user')
1031 log.debug('default user is active, using it as a session user')
1032 user_model.fill_data(self, user_id=anon_user.user_id)
1032 user_model.fill_data(self, user_id=anon_user.user_id)
1033 # then we set this user is logged in
1033 # then we set this user is logged in
1034 self.is_authenticated = True
1034 self.is_authenticated = True
1035 else:
1035 else:
1036 log.debug('default user is NOT active')
1036 log.debug('default user is NOT active')
1037 # in case of disabled anonymous user we reset some of the
1037 # in case of disabled anonymous user we reset some of the
1038 # parameters so such user is "corrupted", skipping the fill_data
1038 # parameters so such user is "corrupted", skipping the fill_data
1039 for attr in ['user_id', 'username', 'admin', 'active']:
1039 for attr in ['user_id', 'username', 'admin', 'active']:
1040 setattr(self, attr, None)
1040 setattr(self, attr, None)
1041 self.is_authenticated = False
1041 self.is_authenticated = False
1042
1042
1043 if not self.username:
1043 if not self.username:
1044 self.username = 'None'
1044 self.username = 'None'
1045
1045
1046 log.debug('AuthUser: propagated user is now %s', self)
1046 log.debug('AuthUser: propagated user is now %s', self)
1047
1047
1048 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1048 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1049 calculate_super_admin=False, cache=False):
1049 calculate_super_admin=False, cache=False):
1050 """
1050 """
1051 Fills user permission attribute with permissions taken from database
1051 Fills user permission attribute with permissions taken from database
1052 works for permissions given for repositories, and for permissions that
1052 works for permissions given for repositories, and for permissions that
1053 are granted to groups
1053 are granted to groups
1054
1054
1055 :param user: instance of User object from database
1055 :param user: instance of User object from database
1056 :param explicit: In case there are permissions both for user and a group
1056 :param explicit: In case there are permissions both for user and a group
1057 that user is part of, explicit flag will defiine if user will
1057 that user is part of, explicit flag will defiine if user will
1058 explicitly override permissions from group, if it's False it will
1058 explicitly override permissions from group, if it's False it will
1059 make decision based on the algo
1059 make decision based on the algo
1060 :param algo: algorithm to decide what permission should be choose if
1060 :param algo: algorithm to decide what permission should be choose if
1061 it's multiple defined, eg user in two different groups. It also
1061 it's multiple defined, eg user in two different groups. It also
1062 decides if explicit flag is turned off how to specify the permission
1062 decides if explicit flag is turned off how to specify the permission
1063 for case when user is in a group + have defined separate permission
1063 for case when user is in a group + have defined separate permission
1064 """
1064 """
1065 user_id = user.user_id
1065 user_id = user.user_id
1066 user_is_admin = user.is_admin
1066 user_is_admin = user.is_admin
1067
1067
1068 # inheritance of global permissions like create repo/fork repo etc
1068 # inheritance of global permissions like create repo/fork repo etc
1069 user_inherit_default_permissions = user.inherit_default_permissions
1069 user_inherit_default_permissions = user.inherit_default_permissions
1070
1070
1071 cache_seconds = safe_int(
1071 cache_seconds = safe_int(
1072 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1072 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1073
1073
1074 cache_on = cache or cache_seconds > 0
1074 cache_on = cache or cache_seconds > 0
1075 log.debug(
1075 log.debug(
1076 'Computing PERMISSION tree for user %s scope `%s` '
1076 'Computing PERMISSION tree for user %s scope `%s` '
1077 'with caching: %s[TTL: %ss]' % (user, scope, cache_on, cache_seconds or 0))
1077 'with caching: %s[TTL: %ss]' % (user, scope, cache_on, cache_seconds or 0))
1078
1078
1079 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1079 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1080 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1080 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1081
1081
1082 @region.cache_on_arguments(namespace=cache_namespace_uid,
1082 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1083 should_cache_fn=lambda v: cache_on)
1083 condition=cache_on)
1084 def compute_perm_tree(cache_name,
1084 def compute_perm_tree(cache_name,
1085 user_id, scope, user_is_admin,user_inherit_default_permissions,
1085 user_id, scope, user_is_admin,user_inherit_default_permissions,
1086 explicit, algo, calculate_super_admin):
1086 explicit, algo, calculate_super_admin):
1087 return _cached_perms_data(
1087 return _cached_perms_data(
1088 user_id, scope, user_is_admin, user_inherit_default_permissions,
1088 user_id, scope, user_is_admin, user_inherit_default_permissions,
1089 explicit, algo, calculate_super_admin)
1089 explicit, algo, calculate_super_admin)
1090
1090
1091 start = time.time()
1091 start = time.time()
1092 result = compute_perm_tree('permissions', user_id, scope, user_is_admin,
1092 result = compute_perm_tree('permissions', user_id, scope, user_is_admin,
1093 user_inherit_default_permissions, explicit, algo,
1093 user_inherit_default_permissions, explicit, algo,
1094 calculate_super_admin)
1094 calculate_super_admin)
1095
1095
1096 result_repr = []
1096 result_repr = []
1097 for k in result:
1097 for k in result:
1098 result_repr.append((k, len(result[k])))
1098 result_repr.append((k, len(result[k])))
1099 total = time.time() - start
1099 total = time.time() - start
1100 log.debug('PERMISSION tree for user %s computed in %.3fs: %s' % (
1100 log.debug('PERMISSION tree for user %s computed in %.3fs: %s' % (
1101 user, total, result_repr))
1101 user, total, result_repr))
1102
1102
1103 return result
1103 return result
1104
1104
1105 @property
1105 @property
1106 def is_default(self):
1106 def is_default(self):
1107 return self.username == User.DEFAULT_USER
1107 return self.username == User.DEFAULT_USER
1108
1108
1109 @property
1109 @property
1110 def is_admin(self):
1110 def is_admin(self):
1111 return self.admin
1111 return self.admin
1112
1112
1113 @property
1113 @property
1114 def is_user_object(self):
1114 def is_user_object(self):
1115 return self.user_id is not None
1115 return self.user_id is not None
1116
1116
1117 @property
1117 @property
1118 def repositories_admin(self):
1118 def repositories_admin(self):
1119 """
1119 """
1120 Returns list of repositories you're an admin of
1120 Returns list of repositories you're an admin of
1121 """
1121 """
1122 return [
1122 return [
1123 x[0] for x in self.permissions['repositories'].items()
1123 x[0] for x in self.permissions['repositories'].items()
1124 if x[1] == 'repository.admin']
1124 if x[1] == 'repository.admin']
1125
1125
1126 @property
1126 @property
1127 def repository_groups_admin(self):
1127 def repository_groups_admin(self):
1128 """
1128 """
1129 Returns list of repository groups you're an admin of
1129 Returns list of repository groups you're an admin of
1130 """
1130 """
1131 return [
1131 return [
1132 x[0] for x in self.permissions['repositories_groups'].items()
1132 x[0] for x in self.permissions['repositories_groups'].items()
1133 if x[1] == 'group.admin']
1133 if x[1] == 'group.admin']
1134
1134
1135 @property
1135 @property
1136 def user_groups_admin(self):
1136 def user_groups_admin(self):
1137 """
1137 """
1138 Returns list of user groups you're an admin of
1138 Returns list of user groups you're an admin of
1139 """
1139 """
1140 return [
1140 return [
1141 x[0] for x in self.permissions['user_groups'].items()
1141 x[0] for x in self.permissions['user_groups'].items()
1142 if x[1] == 'usergroup.admin']
1142 if x[1] == 'usergroup.admin']
1143
1143
1144 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1144 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1145 """
1145 """
1146 Returns list of repository ids that user have access to based on given
1146 Returns list of repository ids that user have access to based on given
1147 perms. The cache flag should be only used in cases that are used for
1147 perms. The cache flag should be only used in cases that are used for
1148 display purposes, NOT IN ANY CASE for permission checks.
1148 display purposes, NOT IN ANY CASE for permission checks.
1149 """
1149 """
1150 from rhodecode.model.scm import RepoList
1150 from rhodecode.model.scm import RepoList
1151 if not perms:
1151 if not perms:
1152 perms = [
1152 perms = [
1153 'repository.read', 'repository.write', 'repository.admin']
1153 'repository.read', 'repository.write', 'repository.admin']
1154
1154
1155 def _cached_repo_acl(user_id, perm_def, _name_filter):
1155 def _cached_repo_acl(user_id, perm_def, _name_filter):
1156 qry = Repository.query()
1156 qry = Repository.query()
1157 if _name_filter:
1157 if _name_filter:
1158 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1158 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1159 qry = qry.filter(
1159 qry = qry.filter(
1160 Repository.repo_name.ilike(ilike_expression))
1160 Repository.repo_name.ilike(ilike_expression))
1161
1161
1162 return [x.repo_id for x in
1162 return [x.repo_id for x in
1163 RepoList(qry, perm_set=perm_def)]
1163 RepoList(qry, perm_set=perm_def)]
1164
1164
1165 return _cached_repo_acl(self.user_id, perms, name_filter)
1165 return _cached_repo_acl(self.user_id, perms, name_filter)
1166
1166
1167 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1167 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1168 """
1168 """
1169 Returns list of repository group ids that user have access to based on given
1169 Returns list of repository group ids that user have access to based on given
1170 perms. The cache flag should be only used in cases that are used for
1170 perms. The cache flag should be only used in cases that are used for
1171 display purposes, NOT IN ANY CASE for permission checks.
1171 display purposes, NOT IN ANY CASE for permission checks.
1172 """
1172 """
1173 from rhodecode.model.scm import RepoGroupList
1173 from rhodecode.model.scm import RepoGroupList
1174 if not perms:
1174 if not perms:
1175 perms = [
1175 perms = [
1176 'group.read', 'group.write', 'group.admin']
1176 'group.read', 'group.write', 'group.admin']
1177
1177
1178 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1178 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1179 qry = RepoGroup.query()
1179 qry = RepoGroup.query()
1180 if _name_filter:
1180 if _name_filter:
1181 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1181 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1182 qry = qry.filter(
1182 qry = qry.filter(
1183 RepoGroup.group_name.ilike(ilike_expression))
1183 RepoGroup.group_name.ilike(ilike_expression))
1184
1184
1185 return [x.group_id for x in
1185 return [x.group_id for x in
1186 RepoGroupList(qry, perm_set=perm_def)]
1186 RepoGroupList(qry, perm_set=perm_def)]
1187
1187
1188 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1188 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1189
1189
1190 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1190 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1191 """
1191 """
1192 Returns list of user group ids that user have access to based on given
1192 Returns list of user group ids that user have access to based on given
1193 perms. The cache flag should be only used in cases that are used for
1193 perms. The cache flag should be only used in cases that are used for
1194 display purposes, NOT IN ANY CASE for permission checks.
1194 display purposes, NOT IN ANY CASE for permission checks.
1195 """
1195 """
1196 from rhodecode.model.scm import UserGroupList
1196 from rhodecode.model.scm import UserGroupList
1197 if not perms:
1197 if not perms:
1198 perms = [
1198 perms = [
1199 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1199 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1200
1200
1201 def _cached_user_group_acl(user_id, perm_def, name_filter):
1201 def _cached_user_group_acl(user_id, perm_def, name_filter):
1202 qry = UserGroup.query()
1202 qry = UserGroup.query()
1203 if name_filter:
1203 if name_filter:
1204 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1204 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1205 qry = qry.filter(
1205 qry = qry.filter(
1206 UserGroup.users_group_name.ilike(ilike_expression))
1206 UserGroup.users_group_name.ilike(ilike_expression))
1207
1207
1208 return [x.users_group_id for x in
1208 return [x.users_group_id for x in
1209 UserGroupList(qry, perm_set=perm_def)]
1209 UserGroupList(qry, perm_set=perm_def)]
1210
1210
1211 return _cached_user_group_acl(self.user_id, perms, name_filter)
1211 return _cached_user_group_acl(self.user_id, perms, name_filter)
1212
1212
1213 @property
1213 @property
1214 def ip_allowed(self):
1214 def ip_allowed(self):
1215 """
1215 """
1216 Checks if ip_addr used in constructor is allowed from defined list of
1216 Checks if ip_addr used in constructor is allowed from defined list of
1217 allowed ip_addresses for user
1217 allowed ip_addresses for user
1218
1218
1219 :returns: boolean, True if ip is in allowed ip range
1219 :returns: boolean, True if ip is in allowed ip range
1220 """
1220 """
1221 # check IP
1221 # check IP
1222 inherit = self.inherit_default_permissions
1222 inherit = self.inherit_default_permissions
1223 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1223 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1224 inherit_from_default=inherit)
1224 inherit_from_default=inherit)
1225 @property
1225 @property
1226 def personal_repo_group(self):
1226 def personal_repo_group(self):
1227 return RepoGroup.get_user_personal_repo_group(self.user_id)
1227 return RepoGroup.get_user_personal_repo_group(self.user_id)
1228
1228
1229 @LazyProperty
1229 @LazyProperty
1230 def feed_token(self):
1230 def feed_token(self):
1231 return self.get_instance().feed_token
1231 return self.get_instance().feed_token
1232
1232
1233 @classmethod
1233 @classmethod
1234 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1234 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1235 allowed_ips = AuthUser.get_allowed_ips(
1235 allowed_ips = AuthUser.get_allowed_ips(
1236 user_id, cache=True, inherit_from_default=inherit_from_default)
1236 user_id, cache=True, inherit_from_default=inherit_from_default)
1237 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1237 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1238 log.debug('IP:%s for user %s is in range of %s' % (
1238 log.debug('IP:%s for user %s is in range of %s' % (
1239 ip_addr, user_id, allowed_ips))
1239 ip_addr, user_id, allowed_ips))
1240 return True
1240 return True
1241 else:
1241 else:
1242 log.info('Access for IP:%s forbidden for user %s, '
1242 log.info('Access for IP:%s forbidden for user %s, '
1243 'not in %s' % (ip_addr, user_id, allowed_ips))
1243 'not in %s' % (ip_addr, user_id, allowed_ips))
1244 return False
1244 return False
1245
1245
1246 def __repr__(self):
1246 def __repr__(self):
1247 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1247 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1248 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1248 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1249
1249
1250 def set_authenticated(self, authenticated=True):
1250 def set_authenticated(self, authenticated=True):
1251 if self.user_id != self.anonymous_user.user_id:
1251 if self.user_id != self.anonymous_user.user_id:
1252 self.is_authenticated = authenticated
1252 self.is_authenticated = authenticated
1253
1253
1254 def get_cookie_store(self):
1254 def get_cookie_store(self):
1255 return {
1255 return {
1256 'username': self.username,
1256 'username': self.username,
1257 'password': md5(self.password or ''),
1257 'password': md5(self.password or ''),
1258 'user_id': self.user_id,
1258 'user_id': self.user_id,
1259 'is_authenticated': self.is_authenticated
1259 'is_authenticated': self.is_authenticated
1260 }
1260 }
1261
1261
1262 @classmethod
1262 @classmethod
1263 def from_cookie_store(cls, cookie_store):
1263 def from_cookie_store(cls, cookie_store):
1264 """
1264 """
1265 Creates AuthUser from a cookie store
1265 Creates AuthUser from a cookie store
1266
1266
1267 :param cls:
1267 :param cls:
1268 :param cookie_store:
1268 :param cookie_store:
1269 """
1269 """
1270 user_id = cookie_store.get('user_id')
1270 user_id = cookie_store.get('user_id')
1271 username = cookie_store.get('username')
1271 username = cookie_store.get('username')
1272 api_key = cookie_store.get('api_key')
1272 api_key = cookie_store.get('api_key')
1273 return AuthUser(user_id, api_key, username)
1273 return AuthUser(user_id, api_key, username)
1274
1274
1275 @classmethod
1275 @classmethod
1276 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1276 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1277 _set = set()
1277 _set = set()
1278
1278
1279 if inherit_from_default:
1279 if inherit_from_default:
1280 default_ips = UserIpMap.query().filter(
1280 def_user_id = User.get_default_user(cache=True).user_id
1281 UserIpMap.user == User.get_default_user(cache=True))
1281 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1282 if cache:
1282 if cache:
1283 default_ips = default_ips.options(
1283 default_ips = default_ips.options(
1284 FromCache("sql_cache_short", "get_user_ips_default"))
1284 FromCache("sql_cache_short", "get_user_ips_default"))
1285
1285
1286 # populate from default user
1286 # populate from default user
1287 for ip in default_ips:
1287 for ip in default_ips:
1288 try:
1288 try:
1289 _set.add(ip.ip_addr)
1289 _set.add(ip.ip_addr)
1290 except ObjectDeletedError:
1290 except ObjectDeletedError:
1291 # since we use heavy caching sometimes it happens that
1291 # since we use heavy caching sometimes it happens that
1292 # we get deleted objects here, we just skip them
1292 # we get deleted objects here, we just skip them
1293 pass
1293 pass
1294
1294
1295 # NOTE:(marcink) we don't want to load any rules for empty
1295 # NOTE:(marcink) we don't want to load any rules for empty
1296 # user_id which is the case of access of non logged users when anonymous
1296 # user_id which is the case of access of non logged users when anonymous
1297 # access is disabled
1297 # access is disabled
1298 user_ips = []
1298 user_ips = []
1299 if user_id:
1299 if user_id:
1300 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1300 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1301 if cache:
1301 if cache:
1302 user_ips = user_ips.options(
1302 user_ips = user_ips.options(
1303 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1303 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1304
1304
1305 for ip in user_ips:
1305 for ip in user_ips:
1306 try:
1306 try:
1307 _set.add(ip.ip_addr)
1307 _set.add(ip.ip_addr)
1308 except ObjectDeletedError:
1308 except ObjectDeletedError:
1309 # since we use heavy caching sometimes it happens that we get
1309 # since we use heavy caching sometimes it happens that we get
1310 # deleted objects here, we just skip them
1310 # deleted objects here, we just skip them
1311 pass
1311 pass
1312 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1312 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1313
1313
1314
1314
1315 def set_available_permissions(settings):
1315 def set_available_permissions(settings):
1316 """
1316 """
1317 This function will propagate pyramid settings with all available defined
1317 This function will propagate pyramid settings with all available defined
1318 permission given in db. We don't want to check each time from db for new
1318 permission given in db. We don't want to check each time from db for new
1319 permissions since adding a new permission also requires application restart
1319 permissions since adding a new permission also requires application restart
1320 ie. to decorate new views with the newly created permission
1320 ie. to decorate new views with the newly created permission
1321
1321
1322 :param settings: current pyramid registry.settings
1322 :param settings: current pyramid registry.settings
1323
1323
1324 """
1324 """
1325 log.debug('auth: getting information about all available permissions')
1325 log.debug('auth: getting information about all available permissions')
1326 try:
1326 try:
1327 sa = meta.Session
1327 sa = meta.Session
1328 all_perms = sa.query(Permission).all()
1328 all_perms = sa.query(Permission).all()
1329 settings.setdefault('available_permissions',
1329 settings.setdefault('available_permissions',
1330 [x.permission_name for x in all_perms])
1330 [x.permission_name for x in all_perms])
1331 log.debug('auth: set available permissions')
1331 log.debug('auth: set available permissions')
1332 except Exception:
1332 except Exception:
1333 log.exception('Failed to fetch permissions from the database.')
1333 log.exception('Failed to fetch permissions from the database.')
1334 raise
1334 raise
1335
1335
1336
1336
1337 def get_csrf_token(session, force_new=False, save_if_missing=True):
1337 def get_csrf_token(session, force_new=False, save_if_missing=True):
1338 """
1338 """
1339 Return the current authentication token, creating one if one doesn't
1339 Return the current authentication token, creating one if one doesn't
1340 already exist and the save_if_missing flag is present.
1340 already exist and the save_if_missing flag is present.
1341
1341
1342 :param session: pass in the pyramid session, else we use the global ones
1342 :param session: pass in the pyramid session, else we use the global ones
1343 :param force_new: force to re-generate the token and store it in session
1343 :param force_new: force to re-generate the token and store it in session
1344 :param save_if_missing: save the newly generated token if it's missing in
1344 :param save_if_missing: save the newly generated token if it's missing in
1345 session
1345 session
1346 """
1346 """
1347 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1347 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1348 # from pyramid.csrf import get_csrf_token
1348 # from pyramid.csrf import get_csrf_token
1349
1349
1350 if (csrf_token_key not in session and save_if_missing) or force_new:
1350 if (csrf_token_key not in session and save_if_missing) or force_new:
1351 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1351 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1352 session[csrf_token_key] = token
1352 session[csrf_token_key] = token
1353 if hasattr(session, 'save'):
1353 if hasattr(session, 'save'):
1354 session.save()
1354 session.save()
1355 return session.get(csrf_token_key)
1355 return session.get(csrf_token_key)
1356
1356
1357
1357
1358 def get_request(perm_class_instance):
1358 def get_request(perm_class_instance):
1359 from pyramid.threadlocal import get_current_request
1359 from pyramid.threadlocal import get_current_request
1360 pyramid_request = get_current_request()
1360 pyramid_request = get_current_request()
1361 return pyramid_request
1361 return pyramid_request
1362
1362
1363
1363
1364 # CHECK DECORATORS
1364 # CHECK DECORATORS
1365 class CSRFRequired(object):
1365 class CSRFRequired(object):
1366 """
1366 """
1367 Decorator for authenticating a form
1367 Decorator for authenticating a form
1368
1368
1369 This decorator uses an authorization token stored in the client's
1369 This decorator uses an authorization token stored in the client's
1370 session for prevention of certain Cross-site request forgery (CSRF)
1370 session for prevention of certain Cross-site request forgery (CSRF)
1371 attacks (See
1371 attacks (See
1372 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1372 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1373 information).
1373 information).
1374
1374
1375 For use with the ``webhelpers.secure_form`` helper functions.
1375 For use with the ``webhelpers.secure_form`` helper functions.
1376
1376
1377 """
1377 """
1378 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1378 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1379 except_methods=None):
1379 except_methods=None):
1380 self.token = token
1380 self.token = token
1381 self.header = header
1381 self.header = header
1382 self.except_methods = except_methods or []
1382 self.except_methods = except_methods or []
1383
1383
1384 def __call__(self, func):
1384 def __call__(self, func):
1385 return get_cython_compat_decorator(self.__wrapper, func)
1385 return get_cython_compat_decorator(self.__wrapper, func)
1386
1386
1387 def _get_csrf(self, _request):
1387 def _get_csrf(self, _request):
1388 return _request.POST.get(self.token, _request.headers.get(self.header))
1388 return _request.POST.get(self.token, _request.headers.get(self.header))
1389
1389
1390 def check_csrf(self, _request, cur_token):
1390 def check_csrf(self, _request, cur_token):
1391 supplied_token = self._get_csrf(_request)
1391 supplied_token = self._get_csrf(_request)
1392 return supplied_token and supplied_token == cur_token
1392 return supplied_token and supplied_token == cur_token
1393
1393
1394 def _get_request(self):
1394 def _get_request(self):
1395 return get_request(self)
1395 return get_request(self)
1396
1396
1397 def __wrapper(self, func, *fargs, **fkwargs):
1397 def __wrapper(self, func, *fargs, **fkwargs):
1398 request = self._get_request()
1398 request = self._get_request()
1399
1399
1400 if request.method in self.except_methods:
1400 if request.method in self.except_methods:
1401 return func(*fargs, **fkwargs)
1401 return func(*fargs, **fkwargs)
1402
1402
1403 cur_token = get_csrf_token(request.session, save_if_missing=False)
1403 cur_token = get_csrf_token(request.session, save_if_missing=False)
1404 if self.check_csrf(request, cur_token):
1404 if self.check_csrf(request, cur_token):
1405 if request.POST.get(self.token):
1405 if request.POST.get(self.token):
1406 del request.POST[self.token]
1406 del request.POST[self.token]
1407 return func(*fargs, **fkwargs)
1407 return func(*fargs, **fkwargs)
1408 else:
1408 else:
1409 reason = 'token-missing'
1409 reason = 'token-missing'
1410 supplied_token = self._get_csrf(request)
1410 supplied_token = self._get_csrf(request)
1411 if supplied_token and cur_token != supplied_token:
1411 if supplied_token and cur_token != supplied_token:
1412 reason = 'token-mismatch [%s:%s]' % (
1412 reason = 'token-mismatch [%s:%s]' % (
1413 cur_token or ''[:6], supplied_token or ''[:6])
1413 cur_token or ''[:6], supplied_token or ''[:6])
1414
1414
1415 csrf_message = \
1415 csrf_message = \
1416 ("Cross-site request forgery detected, request denied. See "
1416 ("Cross-site request forgery detected, request denied. See "
1417 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1417 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1418 "more information.")
1418 "more information.")
1419 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1419 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1420 'REMOTE_ADDR:%s, HEADERS:%s' % (
1420 'REMOTE_ADDR:%s, HEADERS:%s' % (
1421 request, reason, request.remote_addr, request.headers))
1421 request, reason, request.remote_addr, request.headers))
1422
1422
1423 raise HTTPForbidden(explanation=csrf_message)
1423 raise HTTPForbidden(explanation=csrf_message)
1424
1424
1425
1425
1426 class LoginRequired(object):
1426 class LoginRequired(object):
1427 """
1427 """
1428 Must be logged in to execute this function else
1428 Must be logged in to execute this function else
1429 redirect to login page
1429 redirect to login page
1430
1430
1431 :param api_access: if enabled this checks only for valid auth token
1431 :param api_access: if enabled this checks only for valid auth token
1432 and grants access based on valid token
1432 and grants access based on valid token
1433 """
1433 """
1434 def __init__(self, auth_token_access=None):
1434 def __init__(self, auth_token_access=None):
1435 self.auth_token_access = auth_token_access
1435 self.auth_token_access = auth_token_access
1436
1436
1437 def __call__(self, func):
1437 def __call__(self, func):
1438 return get_cython_compat_decorator(self.__wrapper, func)
1438 return get_cython_compat_decorator(self.__wrapper, func)
1439
1439
1440 def _get_request(self):
1440 def _get_request(self):
1441 return get_request(self)
1441 return get_request(self)
1442
1442
1443 def __wrapper(self, func, *fargs, **fkwargs):
1443 def __wrapper(self, func, *fargs, **fkwargs):
1444 from rhodecode.lib import helpers as h
1444 from rhodecode.lib import helpers as h
1445 cls = fargs[0]
1445 cls = fargs[0]
1446 user = cls._rhodecode_user
1446 user = cls._rhodecode_user
1447 request = self._get_request()
1447 request = self._get_request()
1448 _ = request.translate
1448 _ = request.translate
1449
1449
1450 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1450 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1451 log.debug('Starting login restriction checks for user: %s' % (user,))
1451 log.debug('Starting login restriction checks for user: %s' % (user,))
1452 # check if our IP is allowed
1452 # check if our IP is allowed
1453 ip_access_valid = True
1453 ip_access_valid = True
1454 if not user.ip_allowed:
1454 if not user.ip_allowed:
1455 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1455 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1456 category='warning')
1456 category='warning')
1457 ip_access_valid = False
1457 ip_access_valid = False
1458
1458
1459 # check if we used an APIKEY and it's a valid one
1459 # check if we used an APIKEY and it's a valid one
1460 # defined white-list of controllers which API access will be enabled
1460 # defined white-list of controllers which API access will be enabled
1461 _auth_token = request.GET.get(
1461 _auth_token = request.GET.get(
1462 'auth_token', '') or request.GET.get('api_key', '')
1462 'auth_token', '') or request.GET.get('api_key', '')
1463 auth_token_access_valid = allowed_auth_token_access(
1463 auth_token_access_valid = allowed_auth_token_access(
1464 loc, auth_token=_auth_token)
1464 loc, auth_token=_auth_token)
1465
1465
1466 # explicit controller is enabled or API is in our whitelist
1466 # explicit controller is enabled or API is in our whitelist
1467 if self.auth_token_access or auth_token_access_valid:
1467 if self.auth_token_access or auth_token_access_valid:
1468 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1468 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1469 db_user = user.get_instance()
1469 db_user = user.get_instance()
1470
1470
1471 if db_user:
1471 if db_user:
1472 if self.auth_token_access:
1472 if self.auth_token_access:
1473 roles = self.auth_token_access
1473 roles = self.auth_token_access
1474 else:
1474 else:
1475 roles = [UserApiKeys.ROLE_HTTP]
1475 roles = [UserApiKeys.ROLE_HTTP]
1476 token_match = db_user.authenticate_by_token(
1476 token_match = db_user.authenticate_by_token(
1477 _auth_token, roles=roles)
1477 _auth_token, roles=roles)
1478 else:
1478 else:
1479 log.debug('Unable to fetch db instance for auth user: %s', user)
1479 log.debug('Unable to fetch db instance for auth user: %s', user)
1480 token_match = False
1480 token_match = False
1481
1481
1482 if _auth_token and token_match:
1482 if _auth_token and token_match:
1483 auth_token_access_valid = True
1483 auth_token_access_valid = True
1484 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1484 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1485 else:
1485 else:
1486 auth_token_access_valid = False
1486 auth_token_access_valid = False
1487 if not _auth_token:
1487 if not _auth_token:
1488 log.debug("AUTH TOKEN *NOT* present in request")
1488 log.debug("AUTH TOKEN *NOT* present in request")
1489 else:
1489 else:
1490 log.warning(
1490 log.warning(
1491 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1491 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1492
1492
1493 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1493 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1494 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1494 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1495 else 'AUTH_TOKEN_AUTH'
1495 else 'AUTH_TOKEN_AUTH'
1496
1496
1497 if ip_access_valid and (
1497 if ip_access_valid and (
1498 user.is_authenticated or auth_token_access_valid):
1498 user.is_authenticated or auth_token_access_valid):
1499 log.info(
1499 log.info(
1500 'user %s authenticating with:%s IS authenticated on func %s'
1500 'user %s authenticating with:%s IS authenticated on func %s'
1501 % (user, reason, loc))
1501 % (user, reason, loc))
1502
1502
1503 # update user data to check last activity
1503 # update user data to check last activity
1504 user.update_lastactivity()
1504 user.update_lastactivity()
1505 Session().commit()
1505 Session().commit()
1506 return func(*fargs, **fkwargs)
1506 return func(*fargs, **fkwargs)
1507 else:
1507 else:
1508 log.warning(
1508 log.warning(
1509 'user %s authenticating with:%s NOT authenticated on '
1509 'user %s authenticating with:%s NOT authenticated on '
1510 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1510 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1511 % (user, reason, loc, ip_access_valid,
1511 % (user, reason, loc, ip_access_valid,
1512 auth_token_access_valid))
1512 auth_token_access_valid))
1513 # we preserve the get PARAM
1513 # we preserve the get PARAM
1514 came_from = get_came_from(request)
1514 came_from = get_came_from(request)
1515
1515
1516 log.debug('redirecting to login page with %s' % (came_from,))
1516 log.debug('redirecting to login page with %s' % (came_from,))
1517 raise HTTPFound(
1517 raise HTTPFound(
1518 h.route_path('login', _query={'came_from': came_from}))
1518 h.route_path('login', _query={'came_from': came_from}))
1519
1519
1520
1520
1521 class NotAnonymous(object):
1521 class NotAnonymous(object):
1522 """
1522 """
1523 Must be logged in to execute this function else
1523 Must be logged in to execute this function else
1524 redirect to login page
1524 redirect to login page
1525 """
1525 """
1526
1526
1527 def __call__(self, func):
1527 def __call__(self, func):
1528 return get_cython_compat_decorator(self.__wrapper, func)
1528 return get_cython_compat_decorator(self.__wrapper, func)
1529
1529
1530 def _get_request(self):
1530 def _get_request(self):
1531 return get_request(self)
1531 return get_request(self)
1532
1532
1533 def __wrapper(self, func, *fargs, **fkwargs):
1533 def __wrapper(self, func, *fargs, **fkwargs):
1534 import rhodecode.lib.helpers as h
1534 import rhodecode.lib.helpers as h
1535 cls = fargs[0]
1535 cls = fargs[0]
1536 self.user = cls._rhodecode_user
1536 self.user = cls._rhodecode_user
1537 request = self._get_request()
1537 request = self._get_request()
1538 _ = request.translate
1538 _ = request.translate
1539 log.debug('Checking if user is not anonymous @%s' % cls)
1539 log.debug('Checking if user is not anonymous @%s' % cls)
1540
1540
1541 anonymous = self.user.username == User.DEFAULT_USER
1541 anonymous = self.user.username == User.DEFAULT_USER
1542
1542
1543 if anonymous:
1543 if anonymous:
1544 came_from = get_came_from(request)
1544 came_from = get_came_from(request)
1545 h.flash(_('You need to be a registered user to '
1545 h.flash(_('You need to be a registered user to '
1546 'perform this action'),
1546 'perform this action'),
1547 category='warning')
1547 category='warning')
1548 raise HTTPFound(
1548 raise HTTPFound(
1549 h.route_path('login', _query={'came_from': came_from}))
1549 h.route_path('login', _query={'came_from': came_from}))
1550 else:
1550 else:
1551 return func(*fargs, **fkwargs)
1551 return func(*fargs, **fkwargs)
1552
1552
1553
1553
1554 class PermsDecorator(object):
1554 class PermsDecorator(object):
1555 """
1555 """
1556 Base class for controller decorators, we extract the current user from
1556 Base class for controller decorators, we extract the current user from
1557 the class itself, which has it stored in base controllers
1557 the class itself, which has it stored in base controllers
1558 """
1558 """
1559
1559
1560 def __init__(self, *required_perms):
1560 def __init__(self, *required_perms):
1561 self.required_perms = set(required_perms)
1561 self.required_perms = set(required_perms)
1562
1562
1563 def __call__(self, func):
1563 def __call__(self, func):
1564 return get_cython_compat_decorator(self.__wrapper, func)
1564 return get_cython_compat_decorator(self.__wrapper, func)
1565
1565
1566 def _get_request(self):
1566 def _get_request(self):
1567 return get_request(self)
1567 return get_request(self)
1568
1568
1569 def __wrapper(self, func, *fargs, **fkwargs):
1569 def __wrapper(self, func, *fargs, **fkwargs):
1570 import rhodecode.lib.helpers as h
1570 import rhodecode.lib.helpers as h
1571 cls = fargs[0]
1571 cls = fargs[0]
1572 _user = cls._rhodecode_user
1572 _user = cls._rhodecode_user
1573 request = self._get_request()
1573 request = self._get_request()
1574 _ = request.translate
1574 _ = request.translate
1575
1575
1576 log.debug('checking %s permissions %s for %s %s',
1576 log.debug('checking %s permissions %s for %s %s',
1577 self.__class__.__name__, self.required_perms, cls, _user)
1577 self.__class__.__name__, self.required_perms, cls, _user)
1578
1578
1579 if self.check_permissions(_user):
1579 if self.check_permissions(_user):
1580 log.debug('Permission granted for %s %s', cls, _user)
1580 log.debug('Permission granted for %s %s', cls, _user)
1581 return func(*fargs, **fkwargs)
1581 return func(*fargs, **fkwargs)
1582
1582
1583 else:
1583 else:
1584 log.debug('Permission denied for %s %s', cls, _user)
1584 log.debug('Permission denied for %s %s', cls, _user)
1585 anonymous = _user.username == User.DEFAULT_USER
1585 anonymous = _user.username == User.DEFAULT_USER
1586
1586
1587 if anonymous:
1587 if anonymous:
1588 came_from = get_came_from(self._get_request())
1588 came_from = get_came_from(self._get_request())
1589 h.flash(_('You need to be signed in to view this page'),
1589 h.flash(_('You need to be signed in to view this page'),
1590 category='warning')
1590 category='warning')
1591 raise HTTPFound(
1591 raise HTTPFound(
1592 h.route_path('login', _query={'came_from': came_from}))
1592 h.route_path('login', _query={'came_from': came_from}))
1593
1593
1594 else:
1594 else:
1595 # redirect with 404 to prevent resource discovery
1595 # redirect with 404 to prevent resource discovery
1596 raise HTTPNotFound()
1596 raise HTTPNotFound()
1597
1597
1598 def check_permissions(self, user):
1598 def check_permissions(self, user):
1599 """Dummy function for overriding"""
1599 """Dummy function for overriding"""
1600 raise NotImplementedError(
1600 raise NotImplementedError(
1601 'You have to write this function in child class')
1601 'You have to write this function in child class')
1602
1602
1603
1603
1604 class HasPermissionAllDecorator(PermsDecorator):
1604 class HasPermissionAllDecorator(PermsDecorator):
1605 """
1605 """
1606 Checks for access permission for all given predicates. All of them
1606 Checks for access permission for all given predicates. All of them
1607 have to be meet in order to fulfill the request
1607 have to be meet in order to fulfill the request
1608 """
1608 """
1609
1609
1610 def check_permissions(self, user):
1610 def check_permissions(self, user):
1611 perms = user.permissions_with_scope({})
1611 perms = user.permissions_with_scope({})
1612 if self.required_perms.issubset(perms['global']):
1612 if self.required_perms.issubset(perms['global']):
1613 return True
1613 return True
1614 return False
1614 return False
1615
1615
1616
1616
1617 class HasPermissionAnyDecorator(PermsDecorator):
1617 class HasPermissionAnyDecorator(PermsDecorator):
1618 """
1618 """
1619 Checks for access permission for any of given predicates. In order to
1619 Checks for access permission for any of given predicates. In order to
1620 fulfill the request any of predicates must be meet
1620 fulfill the request any of predicates must be meet
1621 """
1621 """
1622
1622
1623 def check_permissions(self, user):
1623 def check_permissions(self, user):
1624 perms = user.permissions_with_scope({})
1624 perms = user.permissions_with_scope({})
1625 if self.required_perms.intersection(perms['global']):
1625 if self.required_perms.intersection(perms['global']):
1626 return True
1626 return True
1627 return False
1627 return False
1628
1628
1629
1629
1630 class HasRepoPermissionAllDecorator(PermsDecorator):
1630 class HasRepoPermissionAllDecorator(PermsDecorator):
1631 """
1631 """
1632 Checks for access permission for all given predicates for specific
1632 Checks for access permission for all given predicates for specific
1633 repository. All of them have to be meet in order to fulfill the request
1633 repository. All of them have to be meet in order to fulfill the request
1634 """
1634 """
1635 def _get_repo_name(self):
1635 def _get_repo_name(self):
1636 _request = self._get_request()
1636 _request = self._get_request()
1637 return get_repo_slug(_request)
1637 return get_repo_slug(_request)
1638
1638
1639 def check_permissions(self, user):
1639 def check_permissions(self, user):
1640 perms = user.permissions
1640 perms = user.permissions
1641 repo_name = self._get_repo_name()
1641 repo_name = self._get_repo_name()
1642
1642
1643 try:
1643 try:
1644 user_perms = {perms['repositories'][repo_name]}
1644 user_perms = {perms['repositories'][repo_name]}
1645 except KeyError:
1645 except KeyError:
1646 log.debug('cannot locate repo with name: `%s` in permissions defs',
1646 log.debug('cannot locate repo with name: `%s` in permissions defs',
1647 repo_name)
1647 repo_name)
1648 return False
1648 return False
1649
1649
1650 log.debug('checking `%s` permissions for repo `%s`',
1650 log.debug('checking `%s` permissions for repo `%s`',
1651 user_perms, repo_name)
1651 user_perms, repo_name)
1652 if self.required_perms.issubset(user_perms):
1652 if self.required_perms.issubset(user_perms):
1653 return True
1653 return True
1654 return False
1654 return False
1655
1655
1656
1656
1657 class HasRepoPermissionAnyDecorator(PermsDecorator):
1657 class HasRepoPermissionAnyDecorator(PermsDecorator):
1658 """
1658 """
1659 Checks for access permission for any of given predicates for specific
1659 Checks for access permission for any of given predicates for specific
1660 repository. In order to fulfill the request any of predicates must be meet
1660 repository. In order to fulfill the request any of predicates must be meet
1661 """
1661 """
1662 def _get_repo_name(self):
1662 def _get_repo_name(self):
1663 _request = self._get_request()
1663 _request = self._get_request()
1664 return get_repo_slug(_request)
1664 return get_repo_slug(_request)
1665
1665
1666 def check_permissions(self, user):
1666 def check_permissions(self, user):
1667 perms = user.permissions
1667 perms = user.permissions
1668 repo_name = self._get_repo_name()
1668 repo_name = self._get_repo_name()
1669
1669
1670 try:
1670 try:
1671 user_perms = {perms['repositories'][repo_name]}
1671 user_perms = {perms['repositories'][repo_name]}
1672 except KeyError:
1672 except KeyError:
1673 log.debug(
1673 log.debug(
1674 'cannot locate repo with name: `%s` in permissions defs',
1674 'cannot locate repo with name: `%s` in permissions defs',
1675 repo_name)
1675 repo_name)
1676 return False
1676 return False
1677
1677
1678 log.debug('checking `%s` permissions for repo `%s`',
1678 log.debug('checking `%s` permissions for repo `%s`',
1679 user_perms, repo_name)
1679 user_perms, repo_name)
1680 if self.required_perms.intersection(user_perms):
1680 if self.required_perms.intersection(user_perms):
1681 return True
1681 return True
1682 return False
1682 return False
1683
1683
1684
1684
1685 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1685 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1686 """
1686 """
1687 Checks for access permission for all given predicates for specific
1687 Checks for access permission for all given predicates for specific
1688 repository group. All of them have to be meet in order to
1688 repository group. All of them have to be meet in order to
1689 fulfill the request
1689 fulfill the request
1690 """
1690 """
1691 def _get_repo_group_name(self):
1691 def _get_repo_group_name(self):
1692 _request = self._get_request()
1692 _request = self._get_request()
1693 return get_repo_group_slug(_request)
1693 return get_repo_group_slug(_request)
1694
1694
1695 def check_permissions(self, user):
1695 def check_permissions(self, user):
1696 perms = user.permissions
1696 perms = user.permissions
1697 group_name = self._get_repo_group_name()
1697 group_name = self._get_repo_group_name()
1698 try:
1698 try:
1699 user_perms = {perms['repositories_groups'][group_name]}
1699 user_perms = {perms['repositories_groups'][group_name]}
1700 except KeyError:
1700 except KeyError:
1701 log.debug(
1701 log.debug(
1702 'cannot locate repo group with name: `%s` in permissions defs',
1702 'cannot locate repo group with name: `%s` in permissions defs',
1703 group_name)
1703 group_name)
1704 return False
1704 return False
1705
1705
1706 log.debug('checking `%s` permissions for repo group `%s`',
1706 log.debug('checking `%s` permissions for repo group `%s`',
1707 user_perms, group_name)
1707 user_perms, group_name)
1708 if self.required_perms.issubset(user_perms):
1708 if self.required_perms.issubset(user_perms):
1709 return True
1709 return True
1710 return False
1710 return False
1711
1711
1712
1712
1713 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1713 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1714 """
1714 """
1715 Checks for access permission for any of given predicates for specific
1715 Checks for access permission for any of given predicates for specific
1716 repository group. In order to fulfill the request any
1716 repository group. In order to fulfill the request any
1717 of predicates must be met
1717 of predicates must be met
1718 """
1718 """
1719 def _get_repo_group_name(self):
1719 def _get_repo_group_name(self):
1720 _request = self._get_request()
1720 _request = self._get_request()
1721 return get_repo_group_slug(_request)
1721 return get_repo_group_slug(_request)
1722
1722
1723 def check_permissions(self, user):
1723 def check_permissions(self, user):
1724 perms = user.permissions
1724 perms = user.permissions
1725 group_name = self._get_repo_group_name()
1725 group_name = self._get_repo_group_name()
1726
1726
1727 try:
1727 try:
1728 user_perms = {perms['repositories_groups'][group_name]}
1728 user_perms = {perms['repositories_groups'][group_name]}
1729 except KeyError:
1729 except KeyError:
1730 log.debug(
1730 log.debug(
1731 'cannot locate repo group with name: `%s` in permissions defs',
1731 'cannot locate repo group with name: `%s` in permissions defs',
1732 group_name)
1732 group_name)
1733 return False
1733 return False
1734
1734
1735 log.debug('checking `%s` permissions for repo group `%s`',
1735 log.debug('checking `%s` permissions for repo group `%s`',
1736 user_perms, group_name)
1736 user_perms, group_name)
1737 if self.required_perms.intersection(user_perms):
1737 if self.required_perms.intersection(user_perms):
1738 return True
1738 return True
1739 return False
1739 return False
1740
1740
1741
1741
1742 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1742 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1743 """
1743 """
1744 Checks for access permission for all given predicates for specific
1744 Checks for access permission for all given predicates for specific
1745 user group. All of them have to be meet in order to fulfill the request
1745 user group. All of them have to be meet in order to fulfill the request
1746 """
1746 """
1747 def _get_user_group_name(self):
1747 def _get_user_group_name(self):
1748 _request = self._get_request()
1748 _request = self._get_request()
1749 return get_user_group_slug(_request)
1749 return get_user_group_slug(_request)
1750
1750
1751 def check_permissions(self, user):
1751 def check_permissions(self, user):
1752 perms = user.permissions
1752 perms = user.permissions
1753 group_name = self._get_user_group_name()
1753 group_name = self._get_user_group_name()
1754 try:
1754 try:
1755 user_perms = {perms['user_groups'][group_name]}
1755 user_perms = {perms['user_groups'][group_name]}
1756 except KeyError:
1756 except KeyError:
1757 return False
1757 return False
1758
1758
1759 if self.required_perms.issubset(user_perms):
1759 if self.required_perms.issubset(user_perms):
1760 return True
1760 return True
1761 return False
1761 return False
1762
1762
1763
1763
1764 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1764 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1765 """
1765 """
1766 Checks for access permission for any of given predicates for specific
1766 Checks for access permission for any of given predicates for specific
1767 user group. In order to fulfill the request any of predicates must be meet
1767 user group. In order to fulfill the request any of predicates must be meet
1768 """
1768 """
1769 def _get_user_group_name(self):
1769 def _get_user_group_name(self):
1770 _request = self._get_request()
1770 _request = self._get_request()
1771 return get_user_group_slug(_request)
1771 return get_user_group_slug(_request)
1772
1772
1773 def check_permissions(self, user):
1773 def check_permissions(self, user):
1774 perms = user.permissions
1774 perms = user.permissions
1775 group_name = self._get_user_group_name()
1775 group_name = self._get_user_group_name()
1776 try:
1776 try:
1777 user_perms = {perms['user_groups'][group_name]}
1777 user_perms = {perms['user_groups'][group_name]}
1778 except KeyError:
1778 except KeyError:
1779 return False
1779 return False
1780
1780
1781 if self.required_perms.intersection(user_perms):
1781 if self.required_perms.intersection(user_perms):
1782 return True
1782 return True
1783 return False
1783 return False
1784
1784
1785
1785
1786 # CHECK FUNCTIONS
1786 # CHECK FUNCTIONS
1787 class PermsFunction(object):
1787 class PermsFunction(object):
1788 """Base function for other check functions"""
1788 """Base function for other check functions"""
1789
1789
1790 def __init__(self, *perms):
1790 def __init__(self, *perms):
1791 self.required_perms = set(perms)
1791 self.required_perms = set(perms)
1792 self.repo_name = None
1792 self.repo_name = None
1793 self.repo_group_name = None
1793 self.repo_group_name = None
1794 self.user_group_name = None
1794 self.user_group_name = None
1795
1795
1796 def __bool__(self):
1796 def __bool__(self):
1797 frame = inspect.currentframe()
1797 frame = inspect.currentframe()
1798 stack_trace = traceback.format_stack(frame)
1798 stack_trace = traceback.format_stack(frame)
1799 log.error('Checking bool value on a class instance of perm '
1799 log.error('Checking bool value on a class instance of perm '
1800 'function is not allowed: %s' % ''.join(stack_trace))
1800 'function is not allowed: %s' % ''.join(stack_trace))
1801 # rather than throwing errors, here we always return False so if by
1801 # rather than throwing errors, here we always return False so if by
1802 # accident someone checks truth for just an instance it will always end
1802 # accident someone checks truth for just an instance it will always end
1803 # up in returning False
1803 # up in returning False
1804 return False
1804 return False
1805 __nonzero__ = __bool__
1805 __nonzero__ = __bool__
1806
1806
1807 def __call__(self, check_location='', user=None):
1807 def __call__(self, check_location='', user=None):
1808 if not user:
1808 if not user:
1809 log.debug('Using user attribute from global request')
1809 log.debug('Using user attribute from global request')
1810 # TODO: remove this someday,put as user as attribute here
1810 # TODO: remove this someday,put as user as attribute here
1811 request = self._get_request()
1811 request = self._get_request()
1812 user = request.user
1812 user = request.user
1813
1813
1814 # init auth user if not already given
1814 # init auth user if not already given
1815 if not isinstance(user, AuthUser):
1815 if not isinstance(user, AuthUser):
1816 log.debug('Wrapping user %s into AuthUser', user)
1816 log.debug('Wrapping user %s into AuthUser', user)
1817 user = AuthUser(user.user_id)
1817 user = AuthUser(user.user_id)
1818
1818
1819 cls_name = self.__class__.__name__
1819 cls_name = self.__class__.__name__
1820 check_scope = self._get_check_scope(cls_name)
1820 check_scope = self._get_check_scope(cls_name)
1821 check_location = check_location or 'unspecified location'
1821 check_location = check_location or 'unspecified location'
1822
1822
1823 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1823 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1824 self.required_perms, user, check_scope, check_location)
1824 self.required_perms, user, check_scope, check_location)
1825 if not user:
1825 if not user:
1826 log.warning('Empty user given for permission check')
1826 log.warning('Empty user given for permission check')
1827 return False
1827 return False
1828
1828
1829 if self.check_permissions(user):
1829 if self.check_permissions(user):
1830 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1830 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1831 check_scope, user, check_location)
1831 check_scope, user, check_location)
1832 return True
1832 return True
1833
1833
1834 else:
1834 else:
1835 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1835 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1836 check_scope, user, check_location)
1836 check_scope, user, check_location)
1837 return False
1837 return False
1838
1838
1839 def _get_request(self):
1839 def _get_request(self):
1840 return get_request(self)
1840 return get_request(self)
1841
1841
1842 def _get_check_scope(self, cls_name):
1842 def _get_check_scope(self, cls_name):
1843 return {
1843 return {
1844 'HasPermissionAll': 'GLOBAL',
1844 'HasPermissionAll': 'GLOBAL',
1845 'HasPermissionAny': 'GLOBAL',
1845 'HasPermissionAny': 'GLOBAL',
1846 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1846 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1847 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1847 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1848 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1848 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1849 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1849 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1850 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1850 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1851 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1851 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1852 }.get(cls_name, '?:%s' % cls_name)
1852 }.get(cls_name, '?:%s' % cls_name)
1853
1853
1854 def check_permissions(self, user):
1854 def check_permissions(self, user):
1855 """Dummy function for overriding"""
1855 """Dummy function for overriding"""
1856 raise Exception('You have to write this function in child class')
1856 raise Exception('You have to write this function in child class')
1857
1857
1858
1858
1859 class HasPermissionAll(PermsFunction):
1859 class HasPermissionAll(PermsFunction):
1860 def check_permissions(self, user):
1860 def check_permissions(self, user):
1861 perms = user.permissions_with_scope({})
1861 perms = user.permissions_with_scope({})
1862 if self.required_perms.issubset(perms.get('global')):
1862 if self.required_perms.issubset(perms.get('global')):
1863 return True
1863 return True
1864 return False
1864 return False
1865
1865
1866
1866
1867 class HasPermissionAny(PermsFunction):
1867 class HasPermissionAny(PermsFunction):
1868 def check_permissions(self, user):
1868 def check_permissions(self, user):
1869 perms = user.permissions_with_scope({})
1869 perms = user.permissions_with_scope({})
1870 if self.required_perms.intersection(perms.get('global')):
1870 if self.required_perms.intersection(perms.get('global')):
1871 return True
1871 return True
1872 return False
1872 return False
1873
1873
1874
1874
1875 class HasRepoPermissionAll(PermsFunction):
1875 class HasRepoPermissionAll(PermsFunction):
1876 def __call__(self, repo_name=None, check_location='', user=None):
1876 def __call__(self, repo_name=None, check_location='', user=None):
1877 self.repo_name = repo_name
1877 self.repo_name = repo_name
1878 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1878 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1879
1879
1880 def _get_repo_name(self):
1880 def _get_repo_name(self):
1881 if not self.repo_name:
1881 if not self.repo_name:
1882 _request = self._get_request()
1882 _request = self._get_request()
1883 self.repo_name = get_repo_slug(_request)
1883 self.repo_name = get_repo_slug(_request)
1884 return self.repo_name
1884 return self.repo_name
1885
1885
1886 def check_permissions(self, user):
1886 def check_permissions(self, user):
1887 self.repo_name = self._get_repo_name()
1887 self.repo_name = self._get_repo_name()
1888 perms = user.permissions
1888 perms = user.permissions
1889 try:
1889 try:
1890 user_perms = {perms['repositories'][self.repo_name]}
1890 user_perms = {perms['repositories'][self.repo_name]}
1891 except KeyError:
1891 except KeyError:
1892 return False
1892 return False
1893 if self.required_perms.issubset(user_perms):
1893 if self.required_perms.issubset(user_perms):
1894 return True
1894 return True
1895 return False
1895 return False
1896
1896
1897
1897
1898 class HasRepoPermissionAny(PermsFunction):
1898 class HasRepoPermissionAny(PermsFunction):
1899 def __call__(self, repo_name=None, check_location='', user=None):
1899 def __call__(self, repo_name=None, check_location='', user=None):
1900 self.repo_name = repo_name
1900 self.repo_name = repo_name
1901 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1901 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1902
1902
1903 def _get_repo_name(self):
1903 def _get_repo_name(self):
1904 if not self.repo_name:
1904 if not self.repo_name:
1905 _request = self._get_request()
1905 _request = self._get_request()
1906 self.repo_name = get_repo_slug(_request)
1906 self.repo_name = get_repo_slug(_request)
1907 return self.repo_name
1907 return self.repo_name
1908
1908
1909 def check_permissions(self, user):
1909 def check_permissions(self, user):
1910 self.repo_name = self._get_repo_name()
1910 self.repo_name = self._get_repo_name()
1911 perms = user.permissions
1911 perms = user.permissions
1912 try:
1912 try:
1913 user_perms = {perms['repositories'][self.repo_name]}
1913 user_perms = {perms['repositories'][self.repo_name]}
1914 except KeyError:
1914 except KeyError:
1915 return False
1915 return False
1916 if self.required_perms.intersection(user_perms):
1916 if self.required_perms.intersection(user_perms):
1917 return True
1917 return True
1918 return False
1918 return False
1919
1919
1920
1920
1921 class HasRepoGroupPermissionAny(PermsFunction):
1921 class HasRepoGroupPermissionAny(PermsFunction):
1922 def __call__(self, group_name=None, check_location='', user=None):
1922 def __call__(self, group_name=None, check_location='', user=None):
1923 self.repo_group_name = group_name
1923 self.repo_group_name = group_name
1924 return super(HasRepoGroupPermissionAny, self).__call__(
1924 return super(HasRepoGroupPermissionAny, self).__call__(
1925 check_location, user)
1925 check_location, user)
1926
1926
1927 def check_permissions(self, user):
1927 def check_permissions(self, user):
1928 perms = user.permissions
1928 perms = user.permissions
1929 try:
1929 try:
1930 user_perms = {perms['repositories_groups'][self.repo_group_name]}
1930 user_perms = {perms['repositories_groups'][self.repo_group_name]}
1931 except KeyError:
1931 except KeyError:
1932 return False
1932 return False
1933 if self.required_perms.intersection(user_perms):
1933 if self.required_perms.intersection(user_perms):
1934 return True
1934 return True
1935 return False
1935 return False
1936
1936
1937
1937
1938 class HasRepoGroupPermissionAll(PermsFunction):
1938 class HasRepoGroupPermissionAll(PermsFunction):
1939 def __call__(self, group_name=None, check_location='', user=None):
1939 def __call__(self, group_name=None, check_location='', user=None):
1940 self.repo_group_name = group_name
1940 self.repo_group_name = group_name
1941 return super(HasRepoGroupPermissionAll, self).__call__(
1941 return super(HasRepoGroupPermissionAll, self).__call__(
1942 check_location, user)
1942 check_location, user)
1943
1943
1944 def check_permissions(self, user):
1944 def check_permissions(self, user):
1945 perms = user.permissions
1945 perms = user.permissions
1946 try:
1946 try:
1947 user_perms = {perms['repositories_groups'][self.repo_group_name]}
1947 user_perms = {perms['repositories_groups'][self.repo_group_name]}
1948 except KeyError:
1948 except KeyError:
1949 return False
1949 return False
1950 if self.required_perms.issubset(user_perms):
1950 if self.required_perms.issubset(user_perms):
1951 return True
1951 return True
1952 return False
1952 return False
1953
1953
1954
1954
1955 class HasUserGroupPermissionAny(PermsFunction):
1955 class HasUserGroupPermissionAny(PermsFunction):
1956 def __call__(self, user_group_name=None, check_location='', user=None):
1956 def __call__(self, user_group_name=None, check_location='', user=None):
1957 self.user_group_name = user_group_name
1957 self.user_group_name = user_group_name
1958 return super(HasUserGroupPermissionAny, self).__call__(
1958 return super(HasUserGroupPermissionAny, self).__call__(
1959 check_location, user)
1959 check_location, user)
1960
1960
1961 def check_permissions(self, user):
1961 def check_permissions(self, user):
1962 perms = user.permissions
1962 perms = user.permissions
1963 try:
1963 try:
1964 user_perms = {perms['user_groups'][self.user_group_name]}
1964 user_perms = {perms['user_groups'][self.user_group_name]}
1965 except KeyError:
1965 except KeyError:
1966 return False
1966 return False
1967 if self.required_perms.intersection(user_perms):
1967 if self.required_perms.intersection(user_perms):
1968 return True
1968 return True
1969 return False
1969 return False
1970
1970
1971
1971
1972 class HasUserGroupPermissionAll(PermsFunction):
1972 class HasUserGroupPermissionAll(PermsFunction):
1973 def __call__(self, user_group_name=None, check_location='', user=None):
1973 def __call__(self, user_group_name=None, check_location='', user=None):
1974 self.user_group_name = user_group_name
1974 self.user_group_name = user_group_name
1975 return super(HasUserGroupPermissionAll, self).__call__(
1975 return super(HasUserGroupPermissionAll, self).__call__(
1976 check_location, user)
1976 check_location, user)
1977
1977
1978 def check_permissions(self, user):
1978 def check_permissions(self, user):
1979 perms = user.permissions
1979 perms = user.permissions
1980 try:
1980 try:
1981 user_perms = {perms['user_groups'][self.user_group_name]}
1981 user_perms = {perms['user_groups'][self.user_group_name]}
1982 except KeyError:
1982 except KeyError:
1983 return False
1983 return False
1984 if self.required_perms.issubset(user_perms):
1984 if self.required_perms.issubset(user_perms):
1985 return True
1985 return True
1986 return False
1986 return False
1987
1987
1988
1988
1989 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1989 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1990 class HasPermissionAnyMiddleware(object):
1990 class HasPermissionAnyMiddleware(object):
1991 def __init__(self, *perms):
1991 def __init__(self, *perms):
1992 self.required_perms = set(perms)
1992 self.required_perms = set(perms)
1993
1993
1994 def __call__(self, user, repo_name):
1994 def __call__(self, user, repo_name):
1995 # repo_name MUST be unicode, since we handle keys in permission
1995 # repo_name MUST be unicode, since we handle keys in permission
1996 # dict by unicode
1996 # dict by unicode
1997 repo_name = safe_unicode(repo_name)
1997 repo_name = safe_unicode(repo_name)
1998 user = AuthUser(user.user_id)
1998 user = AuthUser(user.user_id)
1999 log.debug(
1999 log.debug(
2000 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2000 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2001 self.required_perms, user, repo_name)
2001 self.required_perms, user, repo_name)
2002
2002
2003 if self.check_permissions(user, repo_name):
2003 if self.check_permissions(user, repo_name):
2004 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2004 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2005 repo_name, user, 'PermissionMiddleware')
2005 repo_name, user, 'PermissionMiddleware')
2006 return True
2006 return True
2007
2007
2008 else:
2008 else:
2009 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2009 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2010 repo_name, user, 'PermissionMiddleware')
2010 repo_name, user, 'PermissionMiddleware')
2011 return False
2011 return False
2012
2012
2013 def check_permissions(self, user, repo_name):
2013 def check_permissions(self, user, repo_name):
2014 perms = user.permissions_with_scope({'repo_name': repo_name})
2014 perms = user.permissions_with_scope({'repo_name': repo_name})
2015
2015
2016 try:
2016 try:
2017 user_perms = {perms['repositories'][repo_name]}
2017 user_perms = {perms['repositories'][repo_name]}
2018 except Exception:
2018 except Exception:
2019 log.exception('Error while accessing user permissions')
2019 log.exception('Error while accessing user permissions')
2020 return False
2020 return False
2021
2021
2022 if self.required_perms.intersection(user_perms):
2022 if self.required_perms.intersection(user_perms):
2023 return True
2023 return True
2024 return False
2024 return False
2025
2025
2026
2026
2027 # SPECIAL VERSION TO HANDLE API AUTH
2027 # SPECIAL VERSION TO HANDLE API AUTH
2028 class _BaseApiPerm(object):
2028 class _BaseApiPerm(object):
2029 def __init__(self, *perms):
2029 def __init__(self, *perms):
2030 self.required_perms = set(perms)
2030 self.required_perms = set(perms)
2031
2031
2032 def __call__(self, check_location=None, user=None, repo_name=None,
2032 def __call__(self, check_location=None, user=None, repo_name=None,
2033 group_name=None, user_group_name=None):
2033 group_name=None, user_group_name=None):
2034 cls_name = self.__class__.__name__
2034 cls_name = self.__class__.__name__
2035 check_scope = 'global:%s' % (self.required_perms,)
2035 check_scope = 'global:%s' % (self.required_perms,)
2036 if repo_name:
2036 if repo_name:
2037 check_scope += ', repo_name:%s' % (repo_name,)
2037 check_scope += ', repo_name:%s' % (repo_name,)
2038
2038
2039 if group_name:
2039 if group_name:
2040 check_scope += ', repo_group_name:%s' % (group_name,)
2040 check_scope += ', repo_group_name:%s' % (group_name,)
2041
2041
2042 if user_group_name:
2042 if user_group_name:
2043 check_scope += ', user_group_name:%s' % (user_group_name,)
2043 check_scope += ', user_group_name:%s' % (user_group_name,)
2044
2044
2045 log.debug(
2045 log.debug(
2046 'checking cls:%s %s %s @ %s'
2046 'checking cls:%s %s %s @ %s'
2047 % (cls_name, self.required_perms, check_scope, check_location))
2047 % (cls_name, self.required_perms, check_scope, check_location))
2048 if not user:
2048 if not user:
2049 log.debug('Empty User passed into arguments')
2049 log.debug('Empty User passed into arguments')
2050 return False
2050 return False
2051
2051
2052 # process user
2052 # process user
2053 if not isinstance(user, AuthUser):
2053 if not isinstance(user, AuthUser):
2054 user = AuthUser(user.user_id)
2054 user = AuthUser(user.user_id)
2055 if not check_location:
2055 if not check_location:
2056 check_location = 'unspecified'
2056 check_location = 'unspecified'
2057 if self.check_permissions(user.permissions, repo_name, group_name,
2057 if self.check_permissions(user.permissions, repo_name, group_name,
2058 user_group_name):
2058 user_group_name):
2059 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2059 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2060 check_scope, user, check_location)
2060 check_scope, user, check_location)
2061 return True
2061 return True
2062
2062
2063 else:
2063 else:
2064 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2064 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2065 check_scope, user, check_location)
2065 check_scope, user, check_location)
2066 return False
2066 return False
2067
2067
2068 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2068 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2069 user_group_name=None):
2069 user_group_name=None):
2070 """
2070 """
2071 implement in child class should return True if permissions are ok,
2071 implement in child class should return True if permissions are ok,
2072 False otherwise
2072 False otherwise
2073
2073
2074 :param perm_defs: dict with permission definitions
2074 :param perm_defs: dict with permission definitions
2075 :param repo_name: repo name
2075 :param repo_name: repo name
2076 """
2076 """
2077 raise NotImplementedError()
2077 raise NotImplementedError()
2078
2078
2079
2079
2080 class HasPermissionAllApi(_BaseApiPerm):
2080 class HasPermissionAllApi(_BaseApiPerm):
2081 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2081 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2082 user_group_name=None):
2082 user_group_name=None):
2083 if self.required_perms.issubset(perm_defs.get('global')):
2083 if self.required_perms.issubset(perm_defs.get('global')):
2084 return True
2084 return True
2085 return False
2085 return False
2086
2086
2087
2087
2088 class HasPermissionAnyApi(_BaseApiPerm):
2088 class HasPermissionAnyApi(_BaseApiPerm):
2089 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2089 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2090 user_group_name=None):
2090 user_group_name=None):
2091 if self.required_perms.intersection(perm_defs.get('global')):
2091 if self.required_perms.intersection(perm_defs.get('global')):
2092 return True
2092 return True
2093 return False
2093 return False
2094
2094
2095
2095
2096 class HasRepoPermissionAllApi(_BaseApiPerm):
2096 class HasRepoPermissionAllApi(_BaseApiPerm):
2097 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2097 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2098 user_group_name=None):
2098 user_group_name=None):
2099 try:
2099 try:
2100 _user_perms = {perm_defs['repositories'][repo_name]}
2100 _user_perms = {perm_defs['repositories'][repo_name]}
2101 except KeyError:
2101 except KeyError:
2102 log.warning(traceback.format_exc())
2102 log.warning(traceback.format_exc())
2103 return False
2103 return False
2104 if self.required_perms.issubset(_user_perms):
2104 if self.required_perms.issubset(_user_perms):
2105 return True
2105 return True
2106 return False
2106 return False
2107
2107
2108
2108
2109 class HasRepoPermissionAnyApi(_BaseApiPerm):
2109 class HasRepoPermissionAnyApi(_BaseApiPerm):
2110 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2110 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2111 user_group_name=None):
2111 user_group_name=None):
2112 try:
2112 try:
2113 _user_perms = {perm_defs['repositories'][repo_name]}
2113 _user_perms = {perm_defs['repositories'][repo_name]}
2114 except KeyError:
2114 except KeyError:
2115 log.warning(traceback.format_exc())
2115 log.warning(traceback.format_exc())
2116 return False
2116 return False
2117 if self.required_perms.intersection(_user_perms):
2117 if self.required_perms.intersection(_user_perms):
2118 return True
2118 return True
2119 return False
2119 return False
2120
2120
2121
2121
2122 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2122 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2123 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2123 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2124 user_group_name=None):
2124 user_group_name=None):
2125 try:
2125 try:
2126 _user_perms = {perm_defs['repositories_groups'][group_name]}
2126 _user_perms = {perm_defs['repositories_groups'][group_name]}
2127 except KeyError:
2127 except KeyError:
2128 log.warning(traceback.format_exc())
2128 log.warning(traceback.format_exc())
2129 return False
2129 return False
2130 if self.required_perms.intersection(_user_perms):
2130 if self.required_perms.intersection(_user_perms):
2131 return True
2131 return True
2132 return False
2132 return False
2133
2133
2134
2134
2135 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2135 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2136 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2136 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2137 user_group_name=None):
2137 user_group_name=None):
2138 try:
2138 try:
2139 _user_perms = {perm_defs['repositories_groups'][group_name]}
2139 _user_perms = {perm_defs['repositories_groups'][group_name]}
2140 except KeyError:
2140 except KeyError:
2141 log.warning(traceback.format_exc())
2141 log.warning(traceback.format_exc())
2142 return False
2142 return False
2143 if self.required_perms.issubset(_user_perms):
2143 if self.required_perms.issubset(_user_perms):
2144 return True
2144 return True
2145 return False
2145 return False
2146
2146
2147
2147
2148 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2148 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2149 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2149 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2150 user_group_name=None):
2150 user_group_name=None):
2151 try:
2151 try:
2152 _user_perms = {perm_defs['user_groups'][user_group_name]}
2152 _user_perms = {perm_defs['user_groups'][user_group_name]}
2153 except KeyError:
2153 except KeyError:
2154 log.warning(traceback.format_exc())
2154 log.warning(traceback.format_exc())
2155 return False
2155 return False
2156 if self.required_perms.intersection(_user_perms):
2156 if self.required_perms.intersection(_user_perms):
2157 return True
2157 return True
2158 return False
2158 return False
2159
2159
2160
2160
2161 def check_ip_access(source_ip, allowed_ips=None):
2161 def check_ip_access(source_ip, allowed_ips=None):
2162 """
2162 """
2163 Checks if source_ip is a subnet of any of allowed_ips.
2163 Checks if source_ip is a subnet of any of allowed_ips.
2164
2164
2165 :param source_ip:
2165 :param source_ip:
2166 :param allowed_ips: list of allowed ips together with mask
2166 :param allowed_ips: list of allowed ips together with mask
2167 """
2167 """
2168 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
2168 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
2169 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2169 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2170 if isinstance(allowed_ips, (tuple, list, set)):
2170 if isinstance(allowed_ips, (tuple, list, set)):
2171 for ip in allowed_ips:
2171 for ip in allowed_ips:
2172 ip = safe_unicode(ip)
2172 ip = safe_unicode(ip)
2173 try:
2173 try:
2174 network_address = ipaddress.ip_network(ip, strict=False)
2174 network_address = ipaddress.ip_network(ip, strict=False)
2175 if source_ip_address in network_address:
2175 if source_ip_address in network_address:
2176 log.debug('IP %s is network %s' %
2176 log.debug('IP %s is network %s' %
2177 (source_ip_address, network_address))
2177 (source_ip_address, network_address))
2178 return True
2178 return True
2179 # for any case we cannot determine the IP, don't crash just
2179 # for any case we cannot determine the IP, don't crash just
2180 # skip it and log as error, we want to say forbidden still when
2180 # skip it and log as error, we want to say forbidden still when
2181 # sending bad IP
2181 # sending bad IP
2182 except Exception:
2182 except Exception:
2183 log.error(traceback.format_exc())
2183 log.error(traceback.format_exc())
2184 continue
2184 continue
2185 return False
2185 return False
2186
2186
2187
2187
2188 def get_cython_compat_decorator(wrapper, func):
2188 def get_cython_compat_decorator(wrapper, func):
2189 """
2189 """
2190 Creates a cython compatible decorator. The previously used
2190 Creates a cython compatible decorator. The previously used
2191 decorator.decorator() function seems to be incompatible with cython.
2191 decorator.decorator() function seems to be incompatible with cython.
2192
2192
2193 :param wrapper: __wrapper method of the decorator class
2193 :param wrapper: __wrapper method of the decorator class
2194 :param func: decorated function
2194 :param func: decorated function
2195 """
2195 """
2196 @wraps(func)
2196 @wraps(func)
2197 def local_wrapper(*args, **kwds):
2197 def local_wrapper(*args, **kwds):
2198 return wrapper(func, *args, **kwds)
2198 return wrapper(func, *args, **kwds)
2199 local_wrapper.__wrapped__ = func
2199 local_wrapper.__wrapped__ = func
2200 return local_wrapper
2200 return local_wrapper
2201
2201
2202
2202
@@ -1,662 +1,662 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31 from StringIO import StringIO
31 from StringIO import StringIO
32 from lxml import etree
32 from lxml import etree
33
33
34 import time
34 import time
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36
36
37 from pyramid.httpexceptions import (
37 from pyramid.httpexceptions import (
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 from zope.cachedescriptors.property import Lazy as LazyProperty
39 from zope.cachedescriptors.property import Lazy as LazyProperty
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 from rhodecode.lib import caches, rc_cache
43 from rhodecode.lib import caches, rc_cache
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 from rhodecode.lib.base import (
45 from rhodecode.lib.base import (
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.backends import base
54 from rhodecode.lib.vcs.backends import base
55
55
56 from rhodecode.model import meta
56 from rhodecode.model import meta
57 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.db import User, Repository, PullRequest
58 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.scm import ScmModel
59 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.pull_request import PullRequestModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 def extract_svn_txn_id(acl_repo_name, data):
65 def extract_svn_txn_id(acl_repo_name, data):
66 """
66 """
67 Helper method for extraction of svn txn_id from submited XML data during
67 Helper method for extraction of svn txn_id from submited XML data during
68 POST operations
68 POST operations
69 """
69 """
70 try:
70 try:
71 root = etree.fromstring(data)
71 root = etree.fromstring(data)
72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 for el in root:
73 for el in root:
74 if el.tag == '{DAV:}source':
74 if el.tag == '{DAV:}source':
75 for sub_el in el:
75 for sub_el in el:
76 if sub_el.tag == '{DAV:}href':
76 if sub_el.tag == '{DAV:}href':
77 match = pat.search(sub_el.text)
77 match = pat.search(sub_el.text)
78 if match:
78 if match:
79 svn_tx_id = match.groupdict()['txn_id']
79 svn_tx_id = match.groupdict()['txn_id']
80 txn_id = caches.compute_key_from_params(
80 txn_id = caches.compute_key_from_params(
81 acl_repo_name, svn_tx_id)
81 acl_repo_name, svn_tx_id)
82 return txn_id
82 return txn_id
83 except Exception:
83 except Exception:
84 log.exception('Failed to extract txn_id')
84 log.exception('Failed to extract txn_id')
85
85
86
86
87 def initialize_generator(factory):
87 def initialize_generator(factory):
88 """
88 """
89 Initializes the returned generator by draining its first element.
89 Initializes the returned generator by draining its first element.
90
90
91 This can be used to give a generator an initializer, which is the code
91 This can be used to give a generator an initializer, which is the code
92 up to the first yield statement. This decorator enforces that the first
92 up to the first yield statement. This decorator enforces that the first
93 produced element has the value ``"__init__"`` to make its special
93 produced element has the value ``"__init__"`` to make its special
94 purpose very explicit in the using code.
94 purpose very explicit in the using code.
95 """
95 """
96
96
97 @wraps(factory)
97 @wraps(factory)
98 def wrapper(*args, **kwargs):
98 def wrapper(*args, **kwargs):
99 gen = factory(*args, **kwargs)
99 gen = factory(*args, **kwargs)
100 try:
100 try:
101 init = gen.next()
101 init = gen.next()
102 except StopIteration:
102 except StopIteration:
103 raise ValueError('Generator must yield at least one element.')
103 raise ValueError('Generator must yield at least one element.')
104 if init != "__init__":
104 if init != "__init__":
105 raise ValueError('First yielded element must be "__init__".')
105 raise ValueError('First yielded element must be "__init__".')
106 return gen
106 return gen
107 return wrapper
107 return wrapper
108
108
109
109
110 class SimpleVCS(object):
110 class SimpleVCS(object):
111 """Common functionality for SCM HTTP handlers."""
111 """Common functionality for SCM HTTP handlers."""
112
112
113 SCM = 'unknown'
113 SCM = 'unknown'
114
114
115 acl_repo_name = None
115 acl_repo_name = None
116 url_repo_name = None
116 url_repo_name = None
117 vcs_repo_name = None
117 vcs_repo_name = None
118 rc_extras = {}
118 rc_extras = {}
119
119
120 # We have to handle requests to shadow repositories different than requests
120 # We have to handle requests to shadow repositories different than requests
121 # to normal repositories. Therefore we have to distinguish them. To do this
121 # to normal repositories. Therefore we have to distinguish them. To do this
122 # we use this regex which will match only on URLs pointing to shadow
122 # we use this regex which will match only on URLs pointing to shadow
123 # repositories.
123 # repositories.
124 shadow_repo_re = re.compile(
124 shadow_repo_re = re.compile(
125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 '(?P<target>{slug_pat})/' # target repo
126 '(?P<target>{slug_pat})/' # target repo
127 'pull-request/(?P<pr_id>\d+)/' # pull request
127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 'repository$' # shadow repo
128 'repository$' # shadow repo
129 .format(slug_pat=SLUG_RE.pattern))
129 .format(slug_pat=SLUG_RE.pattern))
130
130
131 def __init__(self, config, registry):
131 def __init__(self, config, registry):
132 self.registry = registry
132 self.registry = registry
133 self.config = config
133 self.config = config
134 # re-populated by specialized middleware
134 # re-populated by specialized middleware
135 self.repo_vcs_config = base.Config()
135 self.repo_vcs_config = base.Config()
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137
137
138 registry.rhodecode_settings = self.rhodecode_settings
138 registry.rhodecode_settings = self.rhodecode_settings
139 # authenticate this VCS request using authfunc
139 # authenticate this VCS request using authfunc
140 auth_ret_code_detection = \
140 auth_ret_code_detection = \
141 str2bool(self.config.get('auth_ret_code_detection', False))
141 str2bool(self.config.get('auth_ret_code_detection', False))
142 self.authenticate = BasicAuth(
142 self.authenticate = BasicAuth(
143 '', authenticate, registry, config.get('auth_ret_code'),
143 '', authenticate, registry, config.get('auth_ret_code'),
144 auth_ret_code_detection)
144 auth_ret_code_detection)
145 self.ip_addr = '0.0.0.0'
145 self.ip_addr = '0.0.0.0'
146
146
147 @LazyProperty
147 @LazyProperty
148 def global_vcs_config(self):
148 def global_vcs_config(self):
149 try:
149 try:
150 return VcsSettingsModel().get_ui_settings_as_config_obj()
150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 except Exception:
151 except Exception:
152 return base.Config()
152 return base.Config()
153
153
154 @property
154 @property
155 def base_path(self):
155 def base_path(self):
156 settings_path = self.repo_vcs_config.get(
156 settings_path = self.repo_vcs_config.get(
157 *VcsSettingsModel.PATH_SETTING)
157 *VcsSettingsModel.PATH_SETTING)
158
158
159 if not settings_path:
159 if not settings_path:
160 settings_path = self.global_vcs_config.get(
160 settings_path = self.global_vcs_config.get(
161 *VcsSettingsModel.PATH_SETTING)
161 *VcsSettingsModel.PATH_SETTING)
162
162
163 if not settings_path:
163 if not settings_path:
164 # try, maybe we passed in explicitly as config option
164 # try, maybe we passed in explicitly as config option
165 settings_path = self.config.get('base_path')
165 settings_path = self.config.get('base_path')
166
166
167 if not settings_path:
167 if not settings_path:
168 raise ValueError('FATAL: base_path is empty')
168 raise ValueError('FATAL: base_path is empty')
169 return settings_path
169 return settings_path
170
170
171 def set_repo_names(self, environ):
171 def set_repo_names(self, environ):
172 """
172 """
173 This will populate the attributes acl_repo_name, url_repo_name,
173 This will populate the attributes acl_repo_name, url_repo_name,
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 shadow) repositories all names are equal. In case of requests to a
175 shadow) repositories all names are equal. In case of requests to a
176 shadow repository the acl-name points to the target repo of the pull
176 shadow repository the acl-name points to the target repo of the pull
177 request and the vcs-name points to the shadow repo file system path.
177 request and the vcs-name points to the shadow repo file system path.
178 The url-name is always the URL used by the vcs client program.
178 The url-name is always the URL used by the vcs client program.
179
179
180 Example in case of a shadow repo:
180 Example in case of a shadow repo:
181 acl_repo_name = RepoGroup/MyRepo
181 acl_repo_name = RepoGroup/MyRepo
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 """
184 """
185 # First we set the repo name from URL for all attributes. This is the
185 # First we set the repo name from URL for all attributes. This is the
186 # default if handling normal (non shadow) repo requests.
186 # default if handling normal (non shadow) repo requests.
187 self.url_repo_name = self._get_repository_name(environ)
187 self.url_repo_name = self._get_repository_name(environ)
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 self.is_shadow_repo = False
189 self.is_shadow_repo = False
190
190
191 # Check if this is a request to a shadow repository.
191 # Check if this is a request to a shadow repository.
192 match = self.shadow_repo_re.match(self.url_repo_name)
192 match = self.shadow_repo_re.match(self.url_repo_name)
193 if match:
193 if match:
194 match_dict = match.groupdict()
194 match_dict = match.groupdict()
195
195
196 # Build acl repo name from regex match.
196 # Build acl repo name from regex match.
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
197 acl_repo_name = safe_unicode('{groups}{target}'.format(
198 groups=match_dict['groups'] or '',
198 groups=match_dict['groups'] or '',
199 target=match_dict['target']))
199 target=match_dict['target']))
200
200
201 # Retrieve pull request instance by ID from regex match.
201 # Retrieve pull request instance by ID from regex match.
202 pull_request = PullRequest.get(match_dict['pr_id'])
202 pull_request = PullRequest.get(match_dict['pr_id'])
203
203
204 # Only proceed if we got a pull request and if acl repo name from
204 # Only proceed if we got a pull request and if acl repo name from
205 # URL equals the target repo name of the pull request.
205 # URL equals the target repo name of the pull request.
206 if pull_request and \
206 if pull_request and \
207 (acl_repo_name == pull_request.target_repo.repo_name):
207 (acl_repo_name == pull_request.target_repo.repo_name):
208 repo_id = pull_request.target_repo.repo_id
208 repo_id = pull_request.target_repo.repo_id
209 # Get file system path to shadow repository.
209 # Get file system path to shadow repository.
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 workspace_id = PullRequestModel()._workspace_id(pull_request)
211 target_vcs = pull_request.target_repo.scm_instance()
211 target_vcs = pull_request.target_repo.scm_instance()
212 vcs_repo_name = target_vcs._get_shadow_repository_path(
212 vcs_repo_name = target_vcs._get_shadow_repository_path(
213 repo_id, workspace_id)
213 repo_id, workspace_id)
214
214
215 # Store names for later usage.
215 # Store names for later usage.
216 self.vcs_repo_name = vcs_repo_name
216 self.vcs_repo_name = vcs_repo_name
217 self.acl_repo_name = acl_repo_name
217 self.acl_repo_name = acl_repo_name
218 self.is_shadow_repo = True
218 self.is_shadow_repo = True
219
219
220 log.debug('Setting all VCS repository names: %s', {
220 log.debug('Setting all VCS repository names: %s', {
221 'acl_repo_name': self.acl_repo_name,
221 'acl_repo_name': self.acl_repo_name,
222 'url_repo_name': self.url_repo_name,
222 'url_repo_name': self.url_repo_name,
223 'vcs_repo_name': self.vcs_repo_name,
223 'vcs_repo_name': self.vcs_repo_name,
224 })
224 })
225
225
226 @property
226 @property
227 def scm_app(self):
227 def scm_app(self):
228 custom_implementation = self.config['vcs.scm_app_implementation']
228 custom_implementation = self.config['vcs.scm_app_implementation']
229 if custom_implementation == 'http':
229 if custom_implementation == 'http':
230 log.info('Using HTTP implementation of scm app.')
230 log.info('Using HTTP implementation of scm app.')
231 scm_app_impl = scm_app_http
231 scm_app_impl = scm_app_http
232 else:
232 else:
233 log.info('Using custom implementation of scm_app: "{}"'.format(
233 log.info('Using custom implementation of scm_app: "{}"'.format(
234 custom_implementation))
234 custom_implementation))
235 scm_app_impl = importlib.import_module(custom_implementation)
235 scm_app_impl = importlib.import_module(custom_implementation)
236 return scm_app_impl
236 return scm_app_impl
237
237
238 def _get_by_id(self, repo_name):
238 def _get_by_id(self, repo_name):
239 """
239 """
240 Gets a special pattern _<ID> from clone url and tries to replace it
240 Gets a special pattern _<ID> from clone url and tries to replace it
241 with a repository_name for support of _<ID> non changeable urls
241 with a repository_name for support of _<ID> non changeable urls
242 """
242 """
243
243
244 data = repo_name.split('/')
244 data = repo_name.split('/')
245 if len(data) >= 2:
245 if len(data) >= 2:
246 from rhodecode.model.repo import RepoModel
246 from rhodecode.model.repo import RepoModel
247 by_id_match = RepoModel().get_repo_by_id(repo_name)
247 by_id_match = RepoModel().get_repo_by_id(repo_name)
248 if by_id_match:
248 if by_id_match:
249 data[1] = by_id_match.repo_name
249 data[1] = by_id_match.repo_name
250
250
251 return safe_str('/'.join(data))
251 return safe_str('/'.join(data))
252
252
253 def _invalidate_cache(self, repo_name):
253 def _invalidate_cache(self, repo_name):
254 """
254 """
255 Set's cache for this repository for invalidation on next access
255 Set's cache for this repository for invalidation on next access
256
256
257 :param repo_name: full repo name, also a cache key
257 :param repo_name: full repo name, also a cache key
258 """
258 """
259 ScmModel().mark_for_invalidation(repo_name)
259 ScmModel().mark_for_invalidation(repo_name)
260
260
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 db_repo = Repository.get_by_repo_name(repo_name)
262 db_repo = Repository.get_by_repo_name(repo_name)
263 if not db_repo:
263 if not db_repo:
264 log.debug('Repository `%s` not found inside the database.',
264 log.debug('Repository `%s` not found inside the database.',
265 repo_name)
265 repo_name)
266 return False
266 return False
267
267
268 if db_repo.repo_type != scm_type:
268 if db_repo.repo_type != scm_type:
269 log.warning(
269 log.warning(
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 repo_name, db_repo.repo_type, scm_type)
271 repo_name, db_repo.repo_type, scm_type)
272 return False
272 return False
273
273
274 config = db_repo._config
274 config = db_repo._config
275 config.set('extensions', 'largefiles', '')
275 config.set('extensions', 'largefiles', '')
276 return is_valid_repo(
276 return is_valid_repo(
277 repo_name, base_path,
277 repo_name, base_path,
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279
279
280 def valid_and_active_user(self, user):
280 def valid_and_active_user(self, user):
281 """
281 """
282 Checks if that user is not empty, and if it's actually object it checks
282 Checks if that user is not empty, and if it's actually object it checks
283 if he's active.
283 if he's active.
284
284
285 :param user: user object or None
285 :param user: user object or None
286 :return: boolean
286 :return: boolean
287 """
287 """
288 if user is None:
288 if user is None:
289 return False
289 return False
290
290
291 elif user.active:
291 elif user.active:
292 return True
292 return True
293
293
294 return False
294 return False
295
295
296 @property
296 @property
297 def is_shadow_repo_dir(self):
297 def is_shadow_repo_dir(self):
298 return os.path.isdir(self.vcs_repo_name)
298 return os.path.isdir(self.vcs_repo_name)
299
299
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 """
302 """
303 Checks permissions using action (push/pull) user and repository
303 Checks permissions using action (push/pull) user and repository
304 name. If plugin_cache and ttl is set it will use the plugin which
304 name. If plugin_cache and ttl is set it will use the plugin which
305 authenticated the user to store the cached permissions result for N
305 authenticated the user to store the cached permissions result for N
306 amount of seconds as in cache_ttl
306 amount of seconds as in cache_ttl
307
307
308 :param action: push or pull action
308 :param action: push or pull action
309 :param user: user instance
309 :param user: user instance
310 :param repo_name: repository name
310 :param repo_name: repository name
311 """
311 """
312
312
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 plugin_id, plugin_cache_active, cache_ttl)
314 plugin_id, plugin_cache_active, cache_ttl)
315
315
316 user_id = user.user_id
316 user_id = user.user_id
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319
319
320 @region.cache_on_arguments(namespace=cache_namespace_uid,
320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 expiration_time=cache_ttl,
321 expiration_time=cache_ttl,
322 should_cache_fn=lambda v: plugin_cache_active)
322 condition=plugin_cache_active)
323 def compute_perm_vcs(
323 def compute_perm_vcs(
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325
325
326 log.debug('auth: calculating permission access now...')
326 log.debug('auth: calculating permission access now...')
327 # check IP
327 # check IP
328 inherit = user.inherit_default_permissions
328 inherit = user.inherit_default_permissions
329 ip_allowed = AuthUser.check_ip_allowed(
329 ip_allowed = AuthUser.check_ip_allowed(
330 user_id, ip_addr, inherit_from_default=inherit)
330 user_id, ip_addr, inherit_from_default=inherit)
331 if ip_allowed:
331 if ip_allowed:
332 log.info('Access for IP:%s allowed', ip_addr)
332 log.info('Access for IP:%s allowed', ip_addr)
333 else:
333 else:
334 return False
334 return False
335
335
336 if action == 'push':
336 if action == 'push':
337 perms = ('repository.write', 'repository.admin')
337 perms = ('repository.write', 'repository.admin')
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
339 return False
339 return False
340
340
341 else:
341 else:
342 # any other action need at least read permission
342 # any other action need at least read permission
343 perms = (
343 perms = (
344 'repository.read', 'repository.write', 'repository.admin')
344 'repository.read', 'repository.write', 'repository.admin')
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
346 return False
346 return False
347
347
348 return True
348 return True
349
349
350 start = time.time()
350 start = time.time()
351 log.debug('Running plugin `%s` permissions check', plugin_id)
351 log.debug('Running plugin `%s` permissions check', plugin_id)
352
352
353 # for environ based auth, password can be empty, but then the validation is
353 # for environ based auth, password can be empty, but then the validation is
354 # on the server that fills in the env data needed for authentication
354 # on the server that fills in the env data needed for authentication
355 perm_result = compute_perm_vcs(
355 perm_result = compute_perm_vcs(
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357
357
358 auth_time = time.time() - start
358 auth_time = time.time() - start
359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
360 'expiration time of fetched cache %.1fs.',
360 'expiration time of fetched cache %.1fs.',
361 plugin_id, auth_time, cache_ttl)
361 plugin_id, auth_time, cache_ttl)
362
362
363 return perm_result
363 return perm_result
364
364
365 def _check_ssl(self, environ, start_response):
365 def _check_ssl(self, environ, start_response):
366 """
366 """
367 Checks the SSL check flag and returns False if SSL is not present
367 Checks the SSL check flag and returns False if SSL is not present
368 and required True otherwise
368 and required True otherwise
369 """
369 """
370 org_proto = environ['wsgi._org_proto']
370 org_proto = environ['wsgi._org_proto']
371 # check if we have SSL required ! if not it's a bad request !
371 # check if we have SSL required ! if not it's a bad request !
372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
373 if require_ssl and org_proto == 'http':
373 if require_ssl and org_proto == 'http':
374 log.debug(
374 log.debug(
375 'Bad request: detected protocol is `%s` and '
375 'Bad request: detected protocol is `%s` and '
376 'SSL/HTTPS is required.', org_proto)
376 'SSL/HTTPS is required.', org_proto)
377 return False
377 return False
378 return True
378 return True
379
379
380 def _get_default_cache_ttl(self):
380 def _get_default_cache_ttl(self):
381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
383 plugin_settings = plugin.get_settings()
383 plugin_settings = plugin.get_settings()
384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
385 plugin_settings) or (False, 0)
385 plugin_settings) or (False, 0)
386 return plugin_cache_active, cache_ttl
386 return plugin_cache_active, cache_ttl
387
387
388 def __call__(self, environ, start_response):
388 def __call__(self, environ, start_response):
389 try:
389 try:
390 return self._handle_request(environ, start_response)
390 return self._handle_request(environ, start_response)
391 except Exception:
391 except Exception:
392 log.exception("Exception while handling request")
392 log.exception("Exception while handling request")
393 appenlight.track_exception(environ)
393 appenlight.track_exception(environ)
394 return HTTPInternalServerError()(environ, start_response)
394 return HTTPInternalServerError()(environ, start_response)
395 finally:
395 finally:
396 meta.Session.remove()
396 meta.Session.remove()
397
397
398 def _handle_request(self, environ, start_response):
398 def _handle_request(self, environ, start_response):
399
399
400 if not self._check_ssl(environ, start_response):
400 if not self._check_ssl(environ, start_response):
401 reason = ('SSL required, while RhodeCode was unable '
401 reason = ('SSL required, while RhodeCode was unable '
402 'to detect this as SSL request')
402 'to detect this as SSL request')
403 log.debug('User not allowed to proceed, %s', reason)
403 log.debug('User not allowed to proceed, %s', reason)
404 return HTTPNotAcceptable(reason)(environ, start_response)
404 return HTTPNotAcceptable(reason)(environ, start_response)
405
405
406 if not self.url_repo_name:
406 if not self.url_repo_name:
407 log.warning('Repository name is empty: %s', self.url_repo_name)
407 log.warning('Repository name is empty: %s', self.url_repo_name)
408 # failed to get repo name, we fail now
408 # failed to get repo name, we fail now
409 return HTTPNotFound()(environ, start_response)
409 return HTTPNotFound()(environ, start_response)
410 log.debug('Extracted repo name is %s', self.url_repo_name)
410 log.debug('Extracted repo name is %s', self.url_repo_name)
411
411
412 ip_addr = get_ip_addr(environ)
412 ip_addr = get_ip_addr(environ)
413 user_agent = get_user_agent(environ)
413 user_agent = get_user_agent(environ)
414 username = None
414 username = None
415
415
416 # skip passing error to error controller
416 # skip passing error to error controller
417 environ['pylons.status_code_redirect'] = True
417 environ['pylons.status_code_redirect'] = True
418
418
419 # ======================================================================
419 # ======================================================================
420 # GET ACTION PULL or PUSH
420 # GET ACTION PULL or PUSH
421 # ======================================================================
421 # ======================================================================
422 action = self._get_action(environ)
422 action = self._get_action(environ)
423
423
424 # ======================================================================
424 # ======================================================================
425 # Check if this is a request to a shadow repository of a pull request.
425 # Check if this is a request to a shadow repository of a pull request.
426 # In this case only pull action is allowed.
426 # In this case only pull action is allowed.
427 # ======================================================================
427 # ======================================================================
428 if self.is_shadow_repo and action != 'pull':
428 if self.is_shadow_repo and action != 'pull':
429 reason = 'Only pull action is allowed for shadow repositories.'
429 reason = 'Only pull action is allowed for shadow repositories.'
430 log.debug('User not allowed to proceed, %s', reason)
430 log.debug('User not allowed to proceed, %s', reason)
431 return HTTPNotAcceptable(reason)(environ, start_response)
431 return HTTPNotAcceptable(reason)(environ, start_response)
432
432
433 # Check if the shadow repo actually exists, in case someone refers
433 # Check if the shadow repo actually exists, in case someone refers
434 # to it, and it has been deleted because of successful merge.
434 # to it, and it has been deleted because of successful merge.
435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
436 log.debug(
436 log.debug(
437 'Shadow repo detected, and shadow repo dir `%s` is missing',
437 'Shadow repo detected, and shadow repo dir `%s` is missing',
438 self.is_shadow_repo_dir)
438 self.is_shadow_repo_dir)
439 return HTTPNotFound()(environ, start_response)
439 return HTTPNotFound()(environ, start_response)
440
440
441 # ======================================================================
441 # ======================================================================
442 # CHECK ANONYMOUS PERMISSION
442 # CHECK ANONYMOUS PERMISSION
443 # ======================================================================
443 # ======================================================================
444 if action in ['pull', 'push']:
444 if action in ['pull', 'push']:
445 anonymous_user = User.get_default_user()
445 anonymous_user = User.get_default_user()
446 username = anonymous_user.username
446 username = anonymous_user.username
447 if anonymous_user.active:
447 if anonymous_user.active:
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
449 # ONLY check permissions if the user is activated
449 # ONLY check permissions if the user is activated
450 anonymous_perm = self._check_permission(
450 anonymous_perm = self._check_permission(
451 action, anonymous_user, self.acl_repo_name, ip_addr,
451 action, anonymous_user, self.acl_repo_name, ip_addr,
452 plugin_id='anonymous_access',
452 plugin_id='anonymous_access',
453 plugin_cache_active=plugin_cache_active,
453 plugin_cache_active=plugin_cache_active,
454 cache_ttl=cache_ttl,
454 cache_ttl=cache_ttl,
455 )
455 )
456 else:
456 else:
457 anonymous_perm = False
457 anonymous_perm = False
458
458
459 if not anonymous_user.active or not anonymous_perm:
459 if not anonymous_user.active or not anonymous_perm:
460 if not anonymous_user.active:
460 if not anonymous_user.active:
461 log.debug('Anonymous access is disabled, running '
461 log.debug('Anonymous access is disabled, running '
462 'authentication')
462 'authentication')
463
463
464 if not anonymous_perm:
464 if not anonymous_perm:
465 log.debug('Not enough credentials to access this '
465 log.debug('Not enough credentials to access this '
466 'repository as anonymous user')
466 'repository as anonymous user')
467
467
468 username = None
468 username = None
469 # ==============================================================
469 # ==============================================================
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
472 # ==============================================================
472 # ==============================================================
473
473
474 # try to auth based on environ, container auth methods
474 # try to auth based on environ, container auth methods
475 log.debug('Running PRE-AUTH for container based authentication')
475 log.debug('Running PRE-AUTH for container based authentication')
476 pre_auth = authenticate(
476 pre_auth = authenticate(
477 '', '', environ, VCS_TYPE, registry=self.registry,
477 '', '', environ, VCS_TYPE, registry=self.registry,
478 acl_repo_name=self.acl_repo_name)
478 acl_repo_name=self.acl_repo_name)
479 if pre_auth and pre_auth.get('username'):
479 if pre_auth and pre_auth.get('username'):
480 username = pre_auth['username']
480 username = pre_auth['username']
481 log.debug('PRE-AUTH got %s as username', username)
481 log.debug('PRE-AUTH got %s as username', username)
482 if pre_auth:
482 if pre_auth:
483 log.debug('PRE-AUTH successful from %s',
483 log.debug('PRE-AUTH successful from %s',
484 pre_auth.get('auth_data', {}).get('_plugin'))
484 pre_auth.get('auth_data', {}).get('_plugin'))
485
485
486 # If not authenticated by the container, running basic auth
486 # If not authenticated by the container, running basic auth
487 # before inject the calling repo_name for special scope checks
487 # before inject the calling repo_name for special scope checks
488 self.authenticate.acl_repo_name = self.acl_repo_name
488 self.authenticate.acl_repo_name = self.acl_repo_name
489
489
490 plugin_cache_active, cache_ttl = False, 0
490 plugin_cache_active, cache_ttl = False, 0
491 plugin = None
491 plugin = None
492 if not username:
492 if not username:
493 self.authenticate.realm = self.authenticate.get_rc_realm()
493 self.authenticate.realm = self.authenticate.get_rc_realm()
494
494
495 try:
495 try:
496 auth_result = self.authenticate(environ)
496 auth_result = self.authenticate(environ)
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
497 except (UserCreationError, NotAllowedToCreateUserError) as e:
498 log.error(e)
498 log.error(e)
499 reason = safe_str(e)
499 reason = safe_str(e)
500 return HTTPNotAcceptable(reason)(environ, start_response)
500 return HTTPNotAcceptable(reason)(environ, start_response)
501
501
502 if isinstance(auth_result, dict):
502 if isinstance(auth_result, dict):
503 AUTH_TYPE.update(environ, 'basic')
503 AUTH_TYPE.update(environ, 'basic')
504 REMOTE_USER.update(environ, auth_result['username'])
504 REMOTE_USER.update(environ, auth_result['username'])
505 username = auth_result['username']
505 username = auth_result['username']
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
506 plugin = auth_result.get('auth_data', {}).get('_plugin')
507 log.info(
507 log.info(
508 'MAIN-AUTH successful for user `%s` from %s plugin',
508 'MAIN-AUTH successful for user `%s` from %s plugin',
509 username, plugin)
509 username, plugin)
510
510
511 plugin_cache_active, cache_ttl = auth_result.get(
511 plugin_cache_active, cache_ttl = auth_result.get(
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
512 'auth_data', {}).get('_ttl_cache') or (False, 0)
513 else:
513 else:
514 return auth_result.wsgi_application(
514 return auth_result.wsgi_application(
515 environ, start_response)
515 environ, start_response)
516
516
517
517
518 # ==============================================================
518 # ==============================================================
519 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
519 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
520 # ==============================================================
520 # ==============================================================
521 user = User.get_by_username(username)
521 user = User.get_by_username(username)
522 if not self.valid_and_active_user(user):
522 if not self.valid_and_active_user(user):
523 return HTTPForbidden()(environ, start_response)
523 return HTTPForbidden()(environ, start_response)
524 username = user.username
524 username = user.username
525 user.update_lastactivity()
525 user.update_lastactivity()
526 meta.Session().commit()
526 meta.Session().commit()
527
527
528 # check user attributes for password change flag
528 # check user attributes for password change flag
529 user_obj = user
529 user_obj = user
530 if user_obj and user_obj.username != User.DEFAULT_USER and \
530 if user_obj and user_obj.username != User.DEFAULT_USER and \
531 user_obj.user_data.get('force_password_change'):
531 user_obj.user_data.get('force_password_change'):
532 reason = 'password change required'
532 reason = 'password change required'
533 log.debug('User not allowed to authenticate, %s', reason)
533 log.debug('User not allowed to authenticate, %s', reason)
534 return HTTPNotAcceptable(reason)(environ, start_response)
534 return HTTPNotAcceptable(reason)(environ, start_response)
535
535
536 # check permissions for this repository
536 # check permissions for this repository
537 perm = self._check_permission(
537 perm = self._check_permission(
538 action, user, self.acl_repo_name, ip_addr,
538 action, user, self.acl_repo_name, ip_addr,
539 plugin, plugin_cache_active, cache_ttl)
539 plugin, plugin_cache_active, cache_ttl)
540 if not perm:
540 if not perm:
541 return HTTPForbidden()(environ, start_response)
541 return HTTPForbidden()(environ, start_response)
542
542
543 # extras are injected into UI object and later available
543 # extras are injected into UI object and later available
544 # in hooks executed by RhodeCode
544 # in hooks executed by RhodeCode
545 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
545 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
546 extras = vcs_operation_context(
546 extras = vcs_operation_context(
547 environ, repo_name=self.acl_repo_name, username=username,
547 environ, repo_name=self.acl_repo_name, username=username,
548 action=action, scm=self.SCM, check_locking=check_locking,
548 action=action, scm=self.SCM, check_locking=check_locking,
549 is_shadow_repo=self.is_shadow_repo
549 is_shadow_repo=self.is_shadow_repo
550 )
550 )
551
551
552 # ======================================================================
552 # ======================================================================
553 # REQUEST HANDLING
553 # REQUEST HANDLING
554 # ======================================================================
554 # ======================================================================
555 repo_path = os.path.join(
555 repo_path = os.path.join(
556 safe_str(self.base_path), safe_str(self.vcs_repo_name))
556 safe_str(self.base_path), safe_str(self.vcs_repo_name))
557 log.debug('Repository path is %s', repo_path)
557 log.debug('Repository path is %s', repo_path)
558
558
559 fix_PATH()
559 fix_PATH()
560
560
561 log.info(
561 log.info(
562 '%s action on %s repo "%s" by "%s" from %s %s',
562 '%s action on %s repo "%s" by "%s" from %s %s',
563 action, self.SCM, safe_str(self.url_repo_name),
563 action, self.SCM, safe_str(self.url_repo_name),
564 safe_str(username), ip_addr, user_agent)
564 safe_str(username), ip_addr, user_agent)
565
565
566 return self._generate_vcs_response(
566 return self._generate_vcs_response(
567 environ, start_response, repo_path, extras, action)
567 environ, start_response, repo_path, extras, action)
568
568
569 @initialize_generator
569 @initialize_generator
570 def _generate_vcs_response(
570 def _generate_vcs_response(
571 self, environ, start_response, repo_path, extras, action):
571 self, environ, start_response, repo_path, extras, action):
572 """
572 """
573 Returns a generator for the response content.
573 Returns a generator for the response content.
574
574
575 This method is implemented as a generator, so that it can trigger
575 This method is implemented as a generator, so that it can trigger
576 the cache validation after all content sent back to the client. It
576 the cache validation after all content sent back to the client. It
577 also handles the locking exceptions which will be triggered when
577 also handles the locking exceptions which will be triggered when
578 the first chunk is produced by the underlying WSGI application.
578 the first chunk is produced by the underlying WSGI application.
579 """
579 """
580 txn_id = ''
580 txn_id = ''
581 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
581 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
582 # case for SVN, we want to re-use the callback daemon port
582 # case for SVN, we want to re-use the callback daemon port
583 # so we use the txn_id, for this we peek the body, and still save
583 # so we use the txn_id, for this we peek the body, and still save
584 # it as wsgi.input
584 # it as wsgi.input
585 data = environ['wsgi.input'].read()
585 data = environ['wsgi.input'].read()
586 environ['wsgi.input'] = StringIO(data)
586 environ['wsgi.input'] = StringIO(data)
587 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
587 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
588
588
589 callback_daemon, extras = self._prepare_callback_daemon(
589 callback_daemon, extras = self._prepare_callback_daemon(
590 extras, environ, action, txn_id=txn_id)
590 extras, environ, action, txn_id=txn_id)
591 log.debug('HOOKS extras is %s', extras)
591 log.debug('HOOKS extras is %s', extras)
592
592
593 config = self._create_config(extras, self.acl_repo_name)
593 config = self._create_config(extras, self.acl_repo_name)
594 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
594 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
595 with callback_daemon:
595 with callback_daemon:
596 app.rc_extras = extras
596 app.rc_extras = extras
597
597
598 try:
598 try:
599 response = app(environ, start_response)
599 response = app(environ, start_response)
600 finally:
600 finally:
601 # This statement works together with the decorator
601 # This statement works together with the decorator
602 # "initialize_generator" above. The decorator ensures that
602 # "initialize_generator" above. The decorator ensures that
603 # we hit the first yield statement before the generator is
603 # we hit the first yield statement before the generator is
604 # returned back to the WSGI server. This is needed to
604 # returned back to the WSGI server. This is needed to
605 # ensure that the call to "app" above triggers the
605 # ensure that the call to "app" above triggers the
606 # needed callback to "start_response" before the
606 # needed callback to "start_response" before the
607 # generator is actually used.
607 # generator is actually used.
608 yield "__init__"
608 yield "__init__"
609
609
610 # iter content
610 # iter content
611 for chunk in response:
611 for chunk in response:
612 yield chunk
612 yield chunk
613
613
614 try:
614 try:
615 # invalidate cache on push
615 # invalidate cache on push
616 if action == 'push':
616 if action == 'push':
617 self._invalidate_cache(self.url_repo_name)
617 self._invalidate_cache(self.url_repo_name)
618 finally:
618 finally:
619 meta.Session.remove()
619 meta.Session.remove()
620
620
621 def _get_repository_name(self, environ):
621 def _get_repository_name(self, environ):
622 """Get repository name out of the environmnent
622 """Get repository name out of the environmnent
623
623
624 :param environ: WSGI environment
624 :param environ: WSGI environment
625 """
625 """
626 raise NotImplementedError()
626 raise NotImplementedError()
627
627
628 def _get_action(self, environ):
628 def _get_action(self, environ):
629 """Map request commands into a pull or push command.
629 """Map request commands into a pull or push command.
630
630
631 :param environ: WSGI environment
631 :param environ: WSGI environment
632 """
632 """
633 raise NotImplementedError()
633 raise NotImplementedError()
634
634
635 def _create_wsgi_app(self, repo_path, repo_name, config):
635 def _create_wsgi_app(self, repo_path, repo_name, config):
636 """Return the WSGI app that will finally handle the request."""
636 """Return the WSGI app that will finally handle the request."""
637 raise NotImplementedError()
637 raise NotImplementedError()
638
638
639 def _create_config(self, extras, repo_name):
639 def _create_config(self, extras, repo_name):
640 """Create a safe config representation."""
640 """Create a safe config representation."""
641 raise NotImplementedError()
641 raise NotImplementedError()
642
642
643 def _should_use_callback_daemon(self, extras, environ, action):
643 def _should_use_callback_daemon(self, extras, environ, action):
644 return True
644 return True
645
645
646 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
646 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
647 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
647 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
648 if not self._should_use_callback_daemon(extras, environ, action):
648 if not self._should_use_callback_daemon(extras, environ, action):
649 # disable callback daemon for actions that don't require it
649 # disable callback daemon for actions that don't require it
650 direct_calls = True
650 direct_calls = True
651
651
652 return prepare_callback_daemon(
652 return prepare_callback_daemon(
653 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
653 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
654 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
654 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
655
655
656
656
657 def _should_check_locking(query_string):
657 def _should_check_locking(query_string):
658 # this is kind of hacky, but due to how mercurial handles client-server
658 # this is kind of hacky, but due to how mercurial handles client-server
659 # server see all operation on commit; bookmarks, phases and
659 # server see all operation on commit; bookmarks, phases and
660 # obsolescence marker in different transaction, we don't want to check
660 # obsolescence marker in different transaction, we don't want to check
661 # locking on those
661 # locking on those
662 return query_string not in ['cmd=listkeys']
662 return query_string not in ['cmd=listkeys']
@@ -1,829 +1,829 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 hashlib
22 import hashlib
23 import logging
23 import logging
24 import time
24 import time
25 from collections import namedtuple
25 from collections import namedtuple
26 from functools import wraps
26 from functools import wraps
27 import bleach
27 import bleach
28
28
29 from rhodecode.lib import caches, rc_cache
29 from rhodecode.lib import caches, rc_cache
30 from rhodecode.lib.utils2 import (
30 from rhodecode.lib.utils2 import (
31 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
31 Optional, AttributeDict, safe_str, remove_prefix, str2bool)
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.model import BaseModel
33 from rhodecode.model import BaseModel
34 from rhodecode.model.db import (
34 from rhodecode.model.db import (
35 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
35 RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, RhodeCodeSetting)
36 from rhodecode.model.meta import Session
36 from rhodecode.model.meta import Session
37
37
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 UiSetting = namedtuple(
42 UiSetting = namedtuple(
43 'UiSetting', ['section', 'key', 'value', 'active'])
43 'UiSetting', ['section', 'key', 'value', 'active'])
44
44
45 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
45 SOCIAL_PLUGINS_LIST = ['github', 'bitbucket', 'twitter', 'google']
46
46
47
47
48 class SettingNotFound(Exception):
48 class SettingNotFound(Exception):
49 def __init__(self, setting_id):
49 def __init__(self, setting_id):
50 msg = 'Setting `{}` is not found'.format(setting_id)
50 msg = 'Setting `{}` is not found'.format(setting_id)
51 super(SettingNotFound, self).__init__(msg)
51 super(SettingNotFound, self).__init__(msg)
52
52
53
53
54 class SettingsModel(BaseModel):
54 class SettingsModel(BaseModel):
55 BUILTIN_HOOKS = (
55 BUILTIN_HOOKS = (
56 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
56 RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH,
57 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
57 RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH,
58 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
58 RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL,
59 RhodeCodeUi.HOOK_PUSH_KEY,)
59 RhodeCodeUi.HOOK_PUSH_KEY,)
60 HOOKS_SECTION = 'hooks'
60 HOOKS_SECTION = 'hooks'
61
61
62 def __init__(self, sa=None, repo=None):
62 def __init__(self, sa=None, repo=None):
63 self.repo = repo
63 self.repo = repo
64 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
64 self.UiDbModel = RepoRhodeCodeUi if repo else RhodeCodeUi
65 self.SettingsDbModel = (
65 self.SettingsDbModel = (
66 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
66 RepoRhodeCodeSetting if repo else RhodeCodeSetting)
67 super(SettingsModel, self).__init__(sa)
67 super(SettingsModel, self).__init__(sa)
68
68
69 def get_ui_by_key(self, key):
69 def get_ui_by_key(self, key):
70 q = self.UiDbModel.query()
70 q = self.UiDbModel.query()
71 q = q.filter(self.UiDbModel.ui_key == key)
71 q = q.filter(self.UiDbModel.ui_key == key)
72 q = self._filter_by_repo(RepoRhodeCodeUi, q)
72 q = self._filter_by_repo(RepoRhodeCodeUi, q)
73 return q.scalar()
73 return q.scalar()
74
74
75 def get_ui_by_section(self, section):
75 def get_ui_by_section(self, section):
76 q = self.UiDbModel.query()
76 q = self.UiDbModel.query()
77 q = q.filter(self.UiDbModel.ui_section == section)
77 q = q.filter(self.UiDbModel.ui_section == section)
78 q = self._filter_by_repo(RepoRhodeCodeUi, q)
78 q = self._filter_by_repo(RepoRhodeCodeUi, q)
79 return q.all()
79 return q.all()
80
80
81 def get_ui_by_section_and_key(self, section, key):
81 def get_ui_by_section_and_key(self, section, key):
82 q = self.UiDbModel.query()
82 q = self.UiDbModel.query()
83 q = q.filter(self.UiDbModel.ui_section == section)
83 q = q.filter(self.UiDbModel.ui_section == section)
84 q = q.filter(self.UiDbModel.ui_key == key)
84 q = q.filter(self.UiDbModel.ui_key == key)
85 q = self._filter_by_repo(RepoRhodeCodeUi, q)
85 q = self._filter_by_repo(RepoRhodeCodeUi, q)
86 return q.scalar()
86 return q.scalar()
87
87
88 def get_ui(self, section=None, key=None):
88 def get_ui(self, section=None, key=None):
89 q = self.UiDbModel.query()
89 q = self.UiDbModel.query()
90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
90 q = self._filter_by_repo(RepoRhodeCodeUi, q)
91
91
92 if section:
92 if section:
93 q = q.filter(self.UiDbModel.ui_section == section)
93 q = q.filter(self.UiDbModel.ui_section == section)
94 if key:
94 if key:
95 q = q.filter(self.UiDbModel.ui_key == key)
95 q = q.filter(self.UiDbModel.ui_key == key)
96
96
97 # TODO: mikhail: add caching
97 # TODO: mikhail: add caching
98 result = [
98 result = [
99 UiSetting(
99 UiSetting(
100 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
100 section=safe_str(r.ui_section), key=safe_str(r.ui_key),
101 value=safe_str(r.ui_value), active=r.ui_active
101 value=safe_str(r.ui_value), active=r.ui_active
102 )
102 )
103 for r in q.all()
103 for r in q.all()
104 ]
104 ]
105 return result
105 return result
106
106
107 def get_builtin_hooks(self):
107 def get_builtin_hooks(self):
108 q = self.UiDbModel.query()
108 q = self.UiDbModel.query()
109 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
109 q = q.filter(self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
110 return self._get_hooks(q)
110 return self._get_hooks(q)
111
111
112 def get_custom_hooks(self):
112 def get_custom_hooks(self):
113 q = self.UiDbModel.query()
113 q = self.UiDbModel.query()
114 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
114 q = q.filter(~self.UiDbModel.ui_key.in_(self.BUILTIN_HOOKS))
115 return self._get_hooks(q)
115 return self._get_hooks(q)
116
116
117 def create_ui_section_value(self, section, val, key=None, active=True):
117 def create_ui_section_value(self, section, val, key=None, active=True):
118 new_ui = self.UiDbModel()
118 new_ui = self.UiDbModel()
119 new_ui.ui_section = section
119 new_ui.ui_section = section
120 new_ui.ui_value = val
120 new_ui.ui_value = val
121 new_ui.ui_active = active
121 new_ui.ui_active = active
122
122
123 if self.repo:
123 if self.repo:
124 repo = self._get_repo(self.repo)
124 repo = self._get_repo(self.repo)
125 repository_id = repo.repo_id
125 repository_id = repo.repo_id
126 new_ui.repository_id = repository_id
126 new_ui.repository_id = repository_id
127
127
128 if not key:
128 if not key:
129 # keys are unique so they need appended info
129 # keys are unique so they need appended info
130 if self.repo:
130 if self.repo:
131 key = hashlib.sha1(
131 key = hashlib.sha1(
132 '{}{}{}'.format(section, val, repository_id)).hexdigest()
132 '{}{}{}'.format(section, val, repository_id)).hexdigest()
133 else:
133 else:
134 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
134 key = hashlib.sha1('{}{}'.format(section, val)).hexdigest()
135
135
136 new_ui.ui_key = key
136 new_ui.ui_key = key
137
137
138 Session().add(new_ui)
138 Session().add(new_ui)
139 return new_ui
139 return new_ui
140
140
141 def create_or_update_hook(self, key, value):
141 def create_or_update_hook(self, key, value):
142 ui = (
142 ui = (
143 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
143 self.get_ui_by_section_and_key(self.HOOKS_SECTION, key) or
144 self.UiDbModel())
144 self.UiDbModel())
145 ui.ui_section = self.HOOKS_SECTION
145 ui.ui_section = self.HOOKS_SECTION
146 ui.ui_active = True
146 ui.ui_active = True
147 ui.ui_key = key
147 ui.ui_key = key
148 ui.ui_value = value
148 ui.ui_value = value
149
149
150 if self.repo:
150 if self.repo:
151 repo = self._get_repo(self.repo)
151 repo = self._get_repo(self.repo)
152 repository_id = repo.repo_id
152 repository_id = repo.repo_id
153 ui.repository_id = repository_id
153 ui.repository_id = repository_id
154
154
155 Session().add(ui)
155 Session().add(ui)
156 return ui
156 return ui
157
157
158 def delete_ui(self, id_):
158 def delete_ui(self, id_):
159 ui = self.UiDbModel.get(id_)
159 ui = self.UiDbModel.get(id_)
160 if not ui:
160 if not ui:
161 raise SettingNotFound(id_)
161 raise SettingNotFound(id_)
162 Session().delete(ui)
162 Session().delete(ui)
163
163
164 def get_setting_by_name(self, name):
164 def get_setting_by_name(self, name):
165 q = self._get_settings_query()
165 q = self._get_settings_query()
166 q = q.filter(self.SettingsDbModel.app_settings_name == name)
166 q = q.filter(self.SettingsDbModel.app_settings_name == name)
167 return q.scalar()
167 return q.scalar()
168
168
169 def create_or_update_setting(
169 def create_or_update_setting(
170 self, name, val=Optional(''), type_=Optional('unicode')):
170 self, name, val=Optional(''), type_=Optional('unicode')):
171 """
171 """
172 Creates or updates RhodeCode setting. If updates is triggered it will
172 Creates or updates RhodeCode setting. If updates is triggered it will
173 only update parameters that are explicityl set Optional instance will
173 only update parameters that are explicityl set Optional instance will
174 be skipped
174 be skipped
175
175
176 :param name:
176 :param name:
177 :param val:
177 :param val:
178 :param type_:
178 :param type_:
179 :return:
179 :return:
180 """
180 """
181
181
182 res = self.get_setting_by_name(name)
182 res = self.get_setting_by_name(name)
183 repo = self._get_repo(self.repo) if self.repo else None
183 repo = self._get_repo(self.repo) if self.repo else None
184
184
185 if not res:
185 if not res:
186 val = Optional.extract(val)
186 val = Optional.extract(val)
187 type_ = Optional.extract(type_)
187 type_ = Optional.extract(type_)
188
188
189 args = (
189 args = (
190 (repo.repo_id, name, val, type_)
190 (repo.repo_id, name, val, type_)
191 if repo else (name, val, type_))
191 if repo else (name, val, type_))
192 res = self.SettingsDbModel(*args)
192 res = self.SettingsDbModel(*args)
193
193
194 else:
194 else:
195 if self.repo:
195 if self.repo:
196 res.repository_id = repo.repo_id
196 res.repository_id = repo.repo_id
197
197
198 res.app_settings_name = name
198 res.app_settings_name = name
199 if not isinstance(type_, Optional):
199 if not isinstance(type_, Optional):
200 # update if set
200 # update if set
201 res.app_settings_type = type_
201 res.app_settings_type = type_
202 if not isinstance(val, Optional):
202 if not isinstance(val, Optional):
203 # update if set
203 # update if set
204 res.app_settings_value = val
204 res.app_settings_value = val
205
205
206 Session().add(res)
206 Session().add(res)
207 return res
207 return res
208
208
209 def invalidate_settings_cache(self):
209 def invalidate_settings_cache(self):
210 # NOTE:(marcink) we flush the whole sql_cache_short region, because it
210 # NOTE:(marcink) we flush the whole sql_cache_short region, because it
211 # reads different settings etc. It's little too much but those caches are
211 # reads different settings etc. It's little too much but those caches are
212 # anyway very short lived and it's a safest way.
212 # anyway very short lived and it's a safest way.
213 region = rc_cache.get_or_create_region('sql_cache_short')
213 region = rc_cache.get_or_create_region('sql_cache_short')
214 region.invalidate()
214 region.invalidate()
215
215
216 def get_all_settings(self, cache=False):
216 def get_all_settings(self, cache=False):
217 region = rc_cache.get_or_create_region('sql_cache_short')
217 region = rc_cache.get_or_create_region('sql_cache_short')
218
218
219 @region.cache_on_arguments(should_cache_fn=lambda v: cache)
219 @region.conditional_cache_on_arguments(condition=cache)
220 def _get_all_settings(name, key):
220 def _get_all_settings(name, key):
221 q = self._get_settings_query()
221 q = self._get_settings_query()
222 if not q:
222 if not q:
223 raise Exception('Could not get application settings !')
223 raise Exception('Could not get application settings !')
224
224
225 settings = {
225 settings = {
226 'rhodecode_' + result.app_settings_name: result.app_settings_value
226 'rhodecode_' + result.app_settings_name: result.app_settings_value
227 for result in q
227 for result in q
228 }
228 }
229 return settings
229 return settings
230
230
231 repo = self._get_repo(self.repo) if self.repo else None
231 repo = self._get_repo(self.repo) if self.repo else None
232 key = "settings_repo.{}".format(repo.repo_id) if repo else "settings_app"
232 key = "settings_repo.{}".format(repo.repo_id) if repo else "settings_app"
233 start = time.time()
233 start = time.time()
234 result = _get_all_settings('rhodecode_settings', key)
234 result = _get_all_settings('rhodecode_settings', key)
235 total = time.time() - start
235 total = time.time() - start
236 log.debug('Fetching app settings for key: %s took: %.3fs', key, total)
236 log.debug('Fetching app settings for key: %s took: %.3fs', key, total)
237
237
238 return result
238 return result
239
239
240 def get_auth_settings(self):
240 def get_auth_settings(self):
241 q = self._get_settings_query()
241 q = self._get_settings_query()
242 q = q.filter(
242 q = q.filter(
243 self.SettingsDbModel.app_settings_name.startswith('auth_'))
243 self.SettingsDbModel.app_settings_name.startswith('auth_'))
244 rows = q.all()
244 rows = q.all()
245 auth_settings = {
245 auth_settings = {
246 row.app_settings_name: row.app_settings_value for row in rows}
246 row.app_settings_name: row.app_settings_value for row in rows}
247 return auth_settings
247 return auth_settings
248
248
249 def get_auth_plugins(self):
249 def get_auth_plugins(self):
250 auth_plugins = self.get_setting_by_name("auth_plugins")
250 auth_plugins = self.get_setting_by_name("auth_plugins")
251 return auth_plugins.app_settings_value
251 return auth_plugins.app_settings_value
252
252
253 def get_default_repo_settings(self, strip_prefix=False):
253 def get_default_repo_settings(self, strip_prefix=False):
254 q = self._get_settings_query()
254 q = self._get_settings_query()
255 q = q.filter(
255 q = q.filter(
256 self.SettingsDbModel.app_settings_name.startswith('default_'))
256 self.SettingsDbModel.app_settings_name.startswith('default_'))
257 rows = q.all()
257 rows = q.all()
258
258
259 result = {}
259 result = {}
260 for row in rows:
260 for row in rows:
261 key = row.app_settings_name
261 key = row.app_settings_name
262 if strip_prefix:
262 if strip_prefix:
263 key = remove_prefix(key, prefix='default_')
263 key = remove_prefix(key, prefix='default_')
264 result.update({key: row.app_settings_value})
264 result.update({key: row.app_settings_value})
265 return result
265 return result
266
266
267 def get_repo(self):
267 def get_repo(self):
268 repo = self._get_repo(self.repo)
268 repo = self._get_repo(self.repo)
269 if not repo:
269 if not repo:
270 raise Exception(
270 raise Exception(
271 'Repository `{}` cannot be found inside the database'.format(
271 'Repository `{}` cannot be found inside the database'.format(
272 self.repo))
272 self.repo))
273 return repo
273 return repo
274
274
275 def _filter_by_repo(self, model, query):
275 def _filter_by_repo(self, model, query):
276 if self.repo:
276 if self.repo:
277 repo = self.get_repo()
277 repo = self.get_repo()
278 query = query.filter(model.repository_id == repo.repo_id)
278 query = query.filter(model.repository_id == repo.repo_id)
279 return query
279 return query
280
280
281 def _get_hooks(self, query):
281 def _get_hooks(self, query):
282 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
282 query = query.filter(self.UiDbModel.ui_section == self.HOOKS_SECTION)
283 query = self._filter_by_repo(RepoRhodeCodeUi, query)
283 query = self._filter_by_repo(RepoRhodeCodeUi, query)
284 return query.all()
284 return query.all()
285
285
286 def _get_settings_query(self):
286 def _get_settings_query(self):
287 q = self.SettingsDbModel.query()
287 q = self.SettingsDbModel.query()
288 return self._filter_by_repo(RepoRhodeCodeSetting, q)
288 return self._filter_by_repo(RepoRhodeCodeSetting, q)
289
289
290 def list_enabled_social_plugins(self, settings):
290 def list_enabled_social_plugins(self, settings):
291 enabled = []
291 enabled = []
292 for plug in SOCIAL_PLUGINS_LIST:
292 for plug in SOCIAL_PLUGINS_LIST:
293 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
293 if str2bool(settings.get('rhodecode_auth_{}_enabled'.format(plug)
294 )):
294 )):
295 enabled.append(plug)
295 enabled.append(plug)
296 return enabled
296 return enabled
297
297
298
298
299 def assert_repo_settings(func):
299 def assert_repo_settings(func):
300 @wraps(func)
300 @wraps(func)
301 def _wrapper(self, *args, **kwargs):
301 def _wrapper(self, *args, **kwargs):
302 if not self.repo_settings:
302 if not self.repo_settings:
303 raise Exception('Repository is not specified')
303 raise Exception('Repository is not specified')
304 return func(self, *args, **kwargs)
304 return func(self, *args, **kwargs)
305 return _wrapper
305 return _wrapper
306
306
307
307
308 class IssueTrackerSettingsModel(object):
308 class IssueTrackerSettingsModel(object):
309 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
309 INHERIT_SETTINGS = 'inherit_issue_tracker_settings'
310 SETTINGS_PREFIX = 'issuetracker_'
310 SETTINGS_PREFIX = 'issuetracker_'
311
311
312 def __init__(self, sa=None, repo=None):
312 def __init__(self, sa=None, repo=None):
313 self.global_settings = SettingsModel(sa=sa)
313 self.global_settings = SettingsModel(sa=sa)
314 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
314 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
315
315
316 @property
316 @property
317 def inherit_global_settings(self):
317 def inherit_global_settings(self):
318 if not self.repo_settings:
318 if not self.repo_settings:
319 return True
319 return True
320 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
320 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
321 return setting.app_settings_value if setting else True
321 return setting.app_settings_value if setting else True
322
322
323 @inherit_global_settings.setter
323 @inherit_global_settings.setter
324 def inherit_global_settings(self, value):
324 def inherit_global_settings(self, value):
325 if self.repo_settings:
325 if self.repo_settings:
326 settings = self.repo_settings.create_or_update_setting(
326 settings = self.repo_settings.create_or_update_setting(
327 self.INHERIT_SETTINGS, value, type_='bool')
327 self.INHERIT_SETTINGS, value, type_='bool')
328 Session().add(settings)
328 Session().add(settings)
329
329
330 def _get_keyname(self, key, uid, prefix=''):
330 def _get_keyname(self, key, uid, prefix=''):
331 return '{0}{1}{2}_{3}'.format(
331 return '{0}{1}{2}_{3}'.format(
332 prefix, self.SETTINGS_PREFIX, key, uid)
332 prefix, self.SETTINGS_PREFIX, key, uid)
333
333
334 def _make_dict_for_settings(self, qs):
334 def _make_dict_for_settings(self, qs):
335 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
335 prefix_match = self._get_keyname('pat', '', 'rhodecode_')
336
336
337 issuetracker_entries = {}
337 issuetracker_entries = {}
338 # create keys
338 # create keys
339 for k, v in qs.items():
339 for k, v in qs.items():
340 if k.startswith(prefix_match):
340 if k.startswith(prefix_match):
341 uid = k[len(prefix_match):]
341 uid = k[len(prefix_match):]
342 issuetracker_entries[uid] = None
342 issuetracker_entries[uid] = None
343
343
344 # populate
344 # populate
345 for uid in issuetracker_entries:
345 for uid in issuetracker_entries:
346 issuetracker_entries[uid] = AttributeDict({
346 issuetracker_entries[uid] = AttributeDict({
347 'pat': qs.get(
347 'pat': qs.get(
348 self._get_keyname('pat', uid, 'rhodecode_')),
348 self._get_keyname('pat', uid, 'rhodecode_')),
349 'url': bleach.clean(
349 'url': bleach.clean(
350 qs.get(self._get_keyname('url', uid, 'rhodecode_')) or ''),
350 qs.get(self._get_keyname('url', uid, 'rhodecode_')) or ''),
351 'pref': bleach.clean(
351 'pref': bleach.clean(
352 qs.get(self._get_keyname('pref', uid, 'rhodecode_')) or ''),
352 qs.get(self._get_keyname('pref', uid, 'rhodecode_')) or ''),
353 'desc': qs.get(
353 'desc': qs.get(
354 self._get_keyname('desc', uid, 'rhodecode_')),
354 self._get_keyname('desc', uid, 'rhodecode_')),
355 })
355 })
356 return issuetracker_entries
356 return issuetracker_entries
357
357
358 def get_global_settings(self, cache=False):
358 def get_global_settings(self, cache=False):
359 """
359 """
360 Returns list of global issue tracker settings
360 Returns list of global issue tracker settings
361 """
361 """
362 defaults = self.global_settings.get_all_settings(cache=cache)
362 defaults = self.global_settings.get_all_settings(cache=cache)
363 settings = self._make_dict_for_settings(defaults)
363 settings = self._make_dict_for_settings(defaults)
364 return settings
364 return settings
365
365
366 def get_repo_settings(self, cache=False):
366 def get_repo_settings(self, cache=False):
367 """
367 """
368 Returns list of issue tracker settings per repository
368 Returns list of issue tracker settings per repository
369 """
369 """
370 if not self.repo_settings:
370 if not self.repo_settings:
371 raise Exception('Repository is not specified')
371 raise Exception('Repository is not specified')
372 all_settings = self.repo_settings.get_all_settings(cache=cache)
372 all_settings = self.repo_settings.get_all_settings(cache=cache)
373 settings = self._make_dict_for_settings(all_settings)
373 settings = self._make_dict_for_settings(all_settings)
374 return settings
374 return settings
375
375
376 def get_settings(self, cache=False):
376 def get_settings(self, cache=False):
377 if self.inherit_global_settings:
377 if self.inherit_global_settings:
378 return self.get_global_settings(cache=cache)
378 return self.get_global_settings(cache=cache)
379 else:
379 else:
380 return self.get_repo_settings(cache=cache)
380 return self.get_repo_settings(cache=cache)
381
381
382 def delete_entries(self, uid):
382 def delete_entries(self, uid):
383 if self.repo_settings:
383 if self.repo_settings:
384 all_patterns = self.get_repo_settings()
384 all_patterns = self.get_repo_settings()
385 settings_model = self.repo_settings
385 settings_model = self.repo_settings
386 else:
386 else:
387 all_patterns = self.get_global_settings()
387 all_patterns = self.get_global_settings()
388 settings_model = self.global_settings
388 settings_model = self.global_settings
389 entries = all_patterns.get(uid, [])
389 entries = all_patterns.get(uid, [])
390
390
391 for del_key in entries:
391 for del_key in entries:
392 setting_name = self._get_keyname(del_key, uid)
392 setting_name = self._get_keyname(del_key, uid)
393 entry = settings_model.get_setting_by_name(setting_name)
393 entry = settings_model.get_setting_by_name(setting_name)
394 if entry:
394 if entry:
395 Session().delete(entry)
395 Session().delete(entry)
396
396
397 Session().commit()
397 Session().commit()
398
398
399 def create_or_update_setting(
399 def create_or_update_setting(
400 self, name, val=Optional(''), type_=Optional('unicode')):
400 self, name, val=Optional(''), type_=Optional('unicode')):
401 if self.repo_settings:
401 if self.repo_settings:
402 setting = self.repo_settings.create_or_update_setting(
402 setting = self.repo_settings.create_or_update_setting(
403 name, val, type_)
403 name, val, type_)
404 else:
404 else:
405 setting = self.global_settings.create_or_update_setting(
405 setting = self.global_settings.create_or_update_setting(
406 name, val, type_)
406 name, val, type_)
407 return setting
407 return setting
408
408
409
409
410 class VcsSettingsModel(object):
410 class VcsSettingsModel(object):
411
411
412 INHERIT_SETTINGS = 'inherit_vcs_settings'
412 INHERIT_SETTINGS = 'inherit_vcs_settings'
413 GENERAL_SETTINGS = (
413 GENERAL_SETTINGS = (
414 'use_outdated_comments',
414 'use_outdated_comments',
415 'pr_merge_enabled',
415 'pr_merge_enabled',
416 'hg_use_rebase_for_merging',
416 'hg_use_rebase_for_merging',
417 'hg_close_branch_before_merging',
417 'hg_close_branch_before_merging',
418 'git_use_rebase_for_merging',
418 'git_use_rebase_for_merging',
419 'git_close_branch_before_merging',
419 'git_close_branch_before_merging',
420 'diff_cache',
420 'diff_cache',
421 )
421 )
422
422
423 HOOKS_SETTINGS = (
423 HOOKS_SETTINGS = (
424 ('hooks', 'changegroup.repo_size'),
424 ('hooks', 'changegroup.repo_size'),
425 ('hooks', 'changegroup.push_logger'),
425 ('hooks', 'changegroup.push_logger'),
426 ('hooks', 'outgoing.pull_logger'),)
426 ('hooks', 'outgoing.pull_logger'),)
427 HG_SETTINGS = (
427 HG_SETTINGS = (
428 ('extensions', 'largefiles'),
428 ('extensions', 'largefiles'),
429 ('phases', 'publish'),
429 ('phases', 'publish'),
430 ('extensions', 'evolve'),)
430 ('extensions', 'evolve'),)
431 GIT_SETTINGS = (
431 GIT_SETTINGS = (
432 ('vcs_git_lfs', 'enabled'),)
432 ('vcs_git_lfs', 'enabled'),)
433 GLOBAL_HG_SETTINGS = (
433 GLOBAL_HG_SETTINGS = (
434 ('extensions', 'largefiles'),
434 ('extensions', 'largefiles'),
435 ('largefiles', 'usercache'),
435 ('largefiles', 'usercache'),
436 ('phases', 'publish'),
436 ('phases', 'publish'),
437 ('extensions', 'hgsubversion'),
437 ('extensions', 'hgsubversion'),
438 ('extensions', 'evolve'),)
438 ('extensions', 'evolve'),)
439 GLOBAL_GIT_SETTINGS = (
439 GLOBAL_GIT_SETTINGS = (
440 ('vcs_git_lfs', 'enabled'),
440 ('vcs_git_lfs', 'enabled'),
441 ('vcs_git_lfs', 'store_location'))
441 ('vcs_git_lfs', 'store_location'))
442
442
443 GLOBAL_SVN_SETTINGS = (
443 GLOBAL_SVN_SETTINGS = (
444 ('vcs_svn_proxy', 'http_requests_enabled'),
444 ('vcs_svn_proxy', 'http_requests_enabled'),
445 ('vcs_svn_proxy', 'http_server_url'))
445 ('vcs_svn_proxy', 'http_server_url'))
446
446
447 SVN_BRANCH_SECTION = 'vcs_svn_branch'
447 SVN_BRANCH_SECTION = 'vcs_svn_branch'
448 SVN_TAG_SECTION = 'vcs_svn_tag'
448 SVN_TAG_SECTION = 'vcs_svn_tag'
449 SSL_SETTING = ('web', 'push_ssl')
449 SSL_SETTING = ('web', 'push_ssl')
450 PATH_SETTING = ('paths', '/')
450 PATH_SETTING = ('paths', '/')
451
451
452 def __init__(self, sa=None, repo=None):
452 def __init__(self, sa=None, repo=None):
453 self.global_settings = SettingsModel(sa=sa)
453 self.global_settings = SettingsModel(sa=sa)
454 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
454 self.repo_settings = SettingsModel(sa=sa, repo=repo) if repo else None
455 self._ui_settings = (
455 self._ui_settings = (
456 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
456 self.HG_SETTINGS + self.GIT_SETTINGS + self.HOOKS_SETTINGS)
457 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
457 self._svn_sections = (self.SVN_BRANCH_SECTION, self.SVN_TAG_SECTION)
458
458
459 @property
459 @property
460 @assert_repo_settings
460 @assert_repo_settings
461 def inherit_global_settings(self):
461 def inherit_global_settings(self):
462 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
462 setting = self.repo_settings.get_setting_by_name(self.INHERIT_SETTINGS)
463 return setting.app_settings_value if setting else True
463 return setting.app_settings_value if setting else True
464
464
465 @inherit_global_settings.setter
465 @inherit_global_settings.setter
466 @assert_repo_settings
466 @assert_repo_settings
467 def inherit_global_settings(self, value):
467 def inherit_global_settings(self, value):
468 self.repo_settings.create_or_update_setting(
468 self.repo_settings.create_or_update_setting(
469 self.INHERIT_SETTINGS, value, type_='bool')
469 self.INHERIT_SETTINGS, value, type_='bool')
470
470
471 def get_global_svn_branch_patterns(self):
471 def get_global_svn_branch_patterns(self):
472 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
472 return self.global_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
473
473
474 @assert_repo_settings
474 @assert_repo_settings
475 def get_repo_svn_branch_patterns(self):
475 def get_repo_svn_branch_patterns(self):
476 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
476 return self.repo_settings.get_ui_by_section(self.SVN_BRANCH_SECTION)
477
477
478 def get_global_svn_tag_patterns(self):
478 def get_global_svn_tag_patterns(self):
479 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
479 return self.global_settings.get_ui_by_section(self.SVN_TAG_SECTION)
480
480
481 @assert_repo_settings
481 @assert_repo_settings
482 def get_repo_svn_tag_patterns(self):
482 def get_repo_svn_tag_patterns(self):
483 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
483 return self.repo_settings.get_ui_by_section(self.SVN_TAG_SECTION)
484
484
485 def get_global_settings(self):
485 def get_global_settings(self):
486 return self._collect_all_settings(global_=True)
486 return self._collect_all_settings(global_=True)
487
487
488 @assert_repo_settings
488 @assert_repo_settings
489 def get_repo_settings(self):
489 def get_repo_settings(self):
490 return self._collect_all_settings(global_=False)
490 return self._collect_all_settings(global_=False)
491
491
492 @assert_repo_settings
492 @assert_repo_settings
493 def create_or_update_repo_settings(
493 def create_or_update_repo_settings(
494 self, data, inherit_global_settings=False):
494 self, data, inherit_global_settings=False):
495 from rhodecode.model.scm import ScmModel
495 from rhodecode.model.scm import ScmModel
496
496
497 self.inherit_global_settings = inherit_global_settings
497 self.inherit_global_settings = inherit_global_settings
498
498
499 repo = self.repo_settings.get_repo()
499 repo = self.repo_settings.get_repo()
500 if not inherit_global_settings:
500 if not inherit_global_settings:
501 if repo.repo_type == 'svn':
501 if repo.repo_type == 'svn':
502 self.create_repo_svn_settings(data)
502 self.create_repo_svn_settings(data)
503 else:
503 else:
504 self.create_or_update_repo_hook_settings(data)
504 self.create_or_update_repo_hook_settings(data)
505 self.create_or_update_repo_pr_settings(data)
505 self.create_or_update_repo_pr_settings(data)
506
506
507 if repo.repo_type == 'hg':
507 if repo.repo_type == 'hg':
508 self.create_or_update_repo_hg_settings(data)
508 self.create_or_update_repo_hg_settings(data)
509
509
510 if repo.repo_type == 'git':
510 if repo.repo_type == 'git':
511 self.create_or_update_repo_git_settings(data)
511 self.create_or_update_repo_git_settings(data)
512
512
513 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
513 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
514
514
515 @assert_repo_settings
515 @assert_repo_settings
516 def create_or_update_repo_hook_settings(self, data):
516 def create_or_update_repo_hook_settings(self, data):
517 for section, key in self.HOOKS_SETTINGS:
517 for section, key in self.HOOKS_SETTINGS:
518 data_key = self._get_form_ui_key(section, key)
518 data_key = self._get_form_ui_key(section, key)
519 if data_key not in data:
519 if data_key not in data:
520 raise ValueError(
520 raise ValueError(
521 'The given data does not contain {} key'.format(data_key))
521 'The given data does not contain {} key'.format(data_key))
522
522
523 active = data.get(data_key)
523 active = data.get(data_key)
524 repo_setting = self.repo_settings.get_ui_by_section_and_key(
524 repo_setting = self.repo_settings.get_ui_by_section_and_key(
525 section, key)
525 section, key)
526 if not repo_setting:
526 if not repo_setting:
527 global_setting = self.global_settings.\
527 global_setting = self.global_settings.\
528 get_ui_by_section_and_key(section, key)
528 get_ui_by_section_and_key(section, key)
529 self.repo_settings.create_ui_section_value(
529 self.repo_settings.create_ui_section_value(
530 section, global_setting.ui_value, key=key, active=active)
530 section, global_setting.ui_value, key=key, active=active)
531 else:
531 else:
532 repo_setting.ui_active = active
532 repo_setting.ui_active = active
533 Session().add(repo_setting)
533 Session().add(repo_setting)
534
534
535 def update_global_hook_settings(self, data):
535 def update_global_hook_settings(self, data):
536 for section, key in self.HOOKS_SETTINGS:
536 for section, key in self.HOOKS_SETTINGS:
537 data_key = self._get_form_ui_key(section, key)
537 data_key = self._get_form_ui_key(section, key)
538 if data_key not in data:
538 if data_key not in data:
539 raise ValueError(
539 raise ValueError(
540 'The given data does not contain {} key'.format(data_key))
540 'The given data does not contain {} key'.format(data_key))
541 active = data.get(data_key)
541 active = data.get(data_key)
542 repo_setting = self.global_settings.get_ui_by_section_and_key(
542 repo_setting = self.global_settings.get_ui_by_section_and_key(
543 section, key)
543 section, key)
544 repo_setting.ui_active = active
544 repo_setting.ui_active = active
545 Session().add(repo_setting)
545 Session().add(repo_setting)
546
546
547 @assert_repo_settings
547 @assert_repo_settings
548 def create_or_update_repo_pr_settings(self, data):
548 def create_or_update_repo_pr_settings(self, data):
549 return self._create_or_update_general_settings(
549 return self._create_or_update_general_settings(
550 self.repo_settings, data)
550 self.repo_settings, data)
551
551
552 def create_or_update_global_pr_settings(self, data):
552 def create_or_update_global_pr_settings(self, data):
553 return self._create_or_update_general_settings(
553 return self._create_or_update_general_settings(
554 self.global_settings, data)
554 self.global_settings, data)
555
555
556 @assert_repo_settings
556 @assert_repo_settings
557 def create_repo_svn_settings(self, data):
557 def create_repo_svn_settings(self, data):
558 return self._create_svn_settings(self.repo_settings, data)
558 return self._create_svn_settings(self.repo_settings, data)
559
559
560 @assert_repo_settings
560 @assert_repo_settings
561 def create_or_update_repo_hg_settings(self, data):
561 def create_or_update_repo_hg_settings(self, data):
562 largefiles, phases, evolve = \
562 largefiles, phases, evolve = \
563 self.HG_SETTINGS
563 self.HG_SETTINGS
564 largefiles_key, phases_key, evolve_key = \
564 largefiles_key, phases_key, evolve_key = \
565 self._get_settings_keys(self.HG_SETTINGS, data)
565 self._get_settings_keys(self.HG_SETTINGS, data)
566
566
567 self._create_or_update_ui(
567 self._create_or_update_ui(
568 self.repo_settings, *largefiles, value='',
568 self.repo_settings, *largefiles, value='',
569 active=data[largefiles_key])
569 active=data[largefiles_key])
570 self._create_or_update_ui(
570 self._create_or_update_ui(
571 self.repo_settings, *evolve, value='',
571 self.repo_settings, *evolve, value='',
572 active=data[evolve_key])
572 active=data[evolve_key])
573 self._create_or_update_ui(
573 self._create_or_update_ui(
574 self.repo_settings, *phases, value=safe_str(data[phases_key]))
574 self.repo_settings, *phases, value=safe_str(data[phases_key]))
575
575
576
576
577 def create_or_update_global_hg_settings(self, data):
577 def create_or_update_global_hg_settings(self, data):
578 largefiles, largefiles_store, phases, hgsubversion, evolve \
578 largefiles, largefiles_store, phases, hgsubversion, evolve \
579 = self.GLOBAL_HG_SETTINGS
579 = self.GLOBAL_HG_SETTINGS
580 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
580 largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
581 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data)
581 = self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data)
582
582
583 self._create_or_update_ui(
583 self._create_or_update_ui(
584 self.global_settings, *largefiles, value='',
584 self.global_settings, *largefiles, value='',
585 active=data[largefiles_key])
585 active=data[largefiles_key])
586 self._create_or_update_ui(
586 self._create_or_update_ui(
587 self.global_settings, *largefiles_store,
587 self.global_settings, *largefiles_store,
588 value=data[largefiles_store_key])
588 value=data[largefiles_store_key])
589 self._create_or_update_ui(
589 self._create_or_update_ui(
590 self.global_settings, *phases, value=safe_str(data[phases_key]))
590 self.global_settings, *phases, value=safe_str(data[phases_key]))
591 self._create_or_update_ui(
591 self._create_or_update_ui(
592 self.global_settings, *hgsubversion, active=data[subversion_key])
592 self.global_settings, *hgsubversion, active=data[subversion_key])
593 self._create_or_update_ui(
593 self._create_or_update_ui(
594 self.global_settings, *evolve, value='',
594 self.global_settings, *evolve, value='',
595 active=data[evolve_key])
595 active=data[evolve_key])
596
596
597 def create_or_update_repo_git_settings(self, data):
597 def create_or_update_repo_git_settings(self, data):
598 # NOTE(marcink): # comma make unpack work properly
598 # NOTE(marcink): # comma make unpack work properly
599 lfs_enabled, \
599 lfs_enabled, \
600 = self.GIT_SETTINGS
600 = self.GIT_SETTINGS
601
601
602 lfs_enabled_key, \
602 lfs_enabled_key, \
603 = self._get_settings_keys(self.GIT_SETTINGS, data)
603 = self._get_settings_keys(self.GIT_SETTINGS, data)
604
604
605 self._create_or_update_ui(
605 self._create_or_update_ui(
606 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
606 self.repo_settings, *lfs_enabled, value=data[lfs_enabled_key],
607 active=data[lfs_enabled_key])
607 active=data[lfs_enabled_key])
608
608
609 def create_or_update_global_git_settings(self, data):
609 def create_or_update_global_git_settings(self, data):
610 lfs_enabled, lfs_store_location \
610 lfs_enabled, lfs_store_location \
611 = self.GLOBAL_GIT_SETTINGS
611 = self.GLOBAL_GIT_SETTINGS
612 lfs_enabled_key, lfs_store_location_key \
612 lfs_enabled_key, lfs_store_location_key \
613 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
613 = self._get_settings_keys(self.GLOBAL_GIT_SETTINGS, data)
614
614
615 self._create_or_update_ui(
615 self._create_or_update_ui(
616 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
616 self.global_settings, *lfs_enabled, value=data[lfs_enabled_key],
617 active=data[lfs_enabled_key])
617 active=data[lfs_enabled_key])
618 self._create_or_update_ui(
618 self._create_or_update_ui(
619 self.global_settings, *lfs_store_location,
619 self.global_settings, *lfs_store_location,
620 value=data[lfs_store_location_key])
620 value=data[lfs_store_location_key])
621
621
622 def create_or_update_global_svn_settings(self, data):
622 def create_or_update_global_svn_settings(self, data):
623 # branch/tags patterns
623 # branch/tags patterns
624 self._create_svn_settings(self.global_settings, data)
624 self._create_svn_settings(self.global_settings, data)
625
625
626 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
626 http_requests_enabled, http_server_url = self.GLOBAL_SVN_SETTINGS
627 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
627 http_requests_enabled_key, http_server_url_key = self._get_settings_keys(
628 self.GLOBAL_SVN_SETTINGS, data)
628 self.GLOBAL_SVN_SETTINGS, data)
629
629
630 self._create_or_update_ui(
630 self._create_or_update_ui(
631 self.global_settings, *http_requests_enabled,
631 self.global_settings, *http_requests_enabled,
632 value=safe_str(data[http_requests_enabled_key]))
632 value=safe_str(data[http_requests_enabled_key]))
633 self._create_or_update_ui(
633 self._create_or_update_ui(
634 self.global_settings, *http_server_url,
634 self.global_settings, *http_server_url,
635 value=data[http_server_url_key])
635 value=data[http_server_url_key])
636
636
637 def update_global_ssl_setting(self, value):
637 def update_global_ssl_setting(self, value):
638 self._create_or_update_ui(
638 self._create_or_update_ui(
639 self.global_settings, *self.SSL_SETTING, value=value)
639 self.global_settings, *self.SSL_SETTING, value=value)
640
640
641 def update_global_path_setting(self, value):
641 def update_global_path_setting(self, value):
642 self._create_or_update_ui(
642 self._create_or_update_ui(
643 self.global_settings, *self.PATH_SETTING, value=value)
643 self.global_settings, *self.PATH_SETTING, value=value)
644
644
645 @assert_repo_settings
645 @assert_repo_settings
646 def delete_repo_svn_pattern(self, id_):
646 def delete_repo_svn_pattern(self, id_):
647 ui = self.repo_settings.UiDbModel.get(id_)
647 ui = self.repo_settings.UiDbModel.get(id_)
648 if ui and ui.repository.repo_name == self.repo_settings.repo:
648 if ui and ui.repository.repo_name == self.repo_settings.repo:
649 # only delete if it's the same repo as initialized settings
649 # only delete if it's the same repo as initialized settings
650 self.repo_settings.delete_ui(id_)
650 self.repo_settings.delete_ui(id_)
651 else:
651 else:
652 # raise error as if we wouldn't find this option
652 # raise error as if we wouldn't find this option
653 self.repo_settings.delete_ui(-1)
653 self.repo_settings.delete_ui(-1)
654
654
655 def delete_global_svn_pattern(self, id_):
655 def delete_global_svn_pattern(self, id_):
656 self.global_settings.delete_ui(id_)
656 self.global_settings.delete_ui(id_)
657
657
658 @assert_repo_settings
658 @assert_repo_settings
659 def get_repo_ui_settings(self, section=None, key=None):
659 def get_repo_ui_settings(self, section=None, key=None):
660 global_uis = self.global_settings.get_ui(section, key)
660 global_uis = self.global_settings.get_ui(section, key)
661 repo_uis = self.repo_settings.get_ui(section, key)
661 repo_uis = self.repo_settings.get_ui(section, key)
662 filtered_repo_uis = self._filter_ui_settings(repo_uis)
662 filtered_repo_uis = self._filter_ui_settings(repo_uis)
663 filtered_repo_uis_keys = [
663 filtered_repo_uis_keys = [
664 (s.section, s.key) for s in filtered_repo_uis]
664 (s.section, s.key) for s in filtered_repo_uis]
665
665
666 def _is_global_ui_filtered(ui):
666 def _is_global_ui_filtered(ui):
667 return (
667 return (
668 (ui.section, ui.key) in filtered_repo_uis_keys
668 (ui.section, ui.key) in filtered_repo_uis_keys
669 or ui.section in self._svn_sections)
669 or ui.section in self._svn_sections)
670
670
671 filtered_global_uis = [
671 filtered_global_uis = [
672 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
672 ui for ui in global_uis if not _is_global_ui_filtered(ui)]
673
673
674 return filtered_global_uis + filtered_repo_uis
674 return filtered_global_uis + filtered_repo_uis
675
675
676 def get_global_ui_settings(self, section=None, key=None):
676 def get_global_ui_settings(self, section=None, key=None):
677 return self.global_settings.get_ui(section, key)
677 return self.global_settings.get_ui(section, key)
678
678
679 def get_ui_settings_as_config_obj(self, section=None, key=None):
679 def get_ui_settings_as_config_obj(self, section=None, key=None):
680 config = base.Config()
680 config = base.Config()
681
681
682 ui_settings = self.get_ui_settings(section=section, key=key)
682 ui_settings = self.get_ui_settings(section=section, key=key)
683
683
684 for entry in ui_settings:
684 for entry in ui_settings:
685 config.set(entry.section, entry.key, entry.value)
685 config.set(entry.section, entry.key, entry.value)
686
686
687 return config
687 return config
688
688
689 def get_ui_settings(self, section=None, key=None):
689 def get_ui_settings(self, section=None, key=None):
690 if not self.repo_settings or self.inherit_global_settings:
690 if not self.repo_settings or self.inherit_global_settings:
691 return self.get_global_ui_settings(section, key)
691 return self.get_global_ui_settings(section, key)
692 else:
692 else:
693 return self.get_repo_ui_settings(section, key)
693 return self.get_repo_ui_settings(section, key)
694
694
695 def get_svn_patterns(self, section=None):
695 def get_svn_patterns(self, section=None):
696 if not self.repo_settings:
696 if not self.repo_settings:
697 return self.get_global_ui_settings(section)
697 return self.get_global_ui_settings(section)
698 else:
698 else:
699 return self.get_repo_ui_settings(section)
699 return self.get_repo_ui_settings(section)
700
700
701 @assert_repo_settings
701 @assert_repo_settings
702 def get_repo_general_settings(self):
702 def get_repo_general_settings(self):
703 global_settings = self.global_settings.get_all_settings()
703 global_settings = self.global_settings.get_all_settings()
704 repo_settings = self.repo_settings.get_all_settings()
704 repo_settings = self.repo_settings.get_all_settings()
705 filtered_repo_settings = self._filter_general_settings(repo_settings)
705 filtered_repo_settings = self._filter_general_settings(repo_settings)
706 global_settings.update(filtered_repo_settings)
706 global_settings.update(filtered_repo_settings)
707 return global_settings
707 return global_settings
708
708
709 def get_global_general_settings(self):
709 def get_global_general_settings(self):
710 return self.global_settings.get_all_settings()
710 return self.global_settings.get_all_settings()
711
711
712 def get_general_settings(self):
712 def get_general_settings(self):
713 if not self.repo_settings or self.inherit_global_settings:
713 if not self.repo_settings or self.inherit_global_settings:
714 return self.get_global_general_settings()
714 return self.get_global_general_settings()
715 else:
715 else:
716 return self.get_repo_general_settings()
716 return self.get_repo_general_settings()
717
717
718 def get_repos_location(self):
718 def get_repos_location(self):
719 return self.global_settings.get_ui_by_key('/').ui_value
719 return self.global_settings.get_ui_by_key('/').ui_value
720
720
721 def _filter_ui_settings(self, settings):
721 def _filter_ui_settings(self, settings):
722 filtered_settings = [
722 filtered_settings = [
723 s for s in settings if self._should_keep_setting(s)]
723 s for s in settings if self._should_keep_setting(s)]
724 return filtered_settings
724 return filtered_settings
725
725
726 def _should_keep_setting(self, setting):
726 def _should_keep_setting(self, setting):
727 keep = (
727 keep = (
728 (setting.section, setting.key) in self._ui_settings or
728 (setting.section, setting.key) in self._ui_settings or
729 setting.section in self._svn_sections)
729 setting.section in self._svn_sections)
730 return keep
730 return keep
731
731
732 def _filter_general_settings(self, settings):
732 def _filter_general_settings(self, settings):
733 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
733 keys = ['rhodecode_{}'.format(key) for key in self.GENERAL_SETTINGS]
734 return {
734 return {
735 k: settings[k]
735 k: settings[k]
736 for k in settings if k in keys}
736 for k in settings if k in keys}
737
737
738 def _collect_all_settings(self, global_=False):
738 def _collect_all_settings(self, global_=False):
739 settings = self.global_settings if global_ else self.repo_settings
739 settings = self.global_settings if global_ else self.repo_settings
740 result = {}
740 result = {}
741
741
742 for section, key in self._ui_settings:
742 for section, key in self._ui_settings:
743 ui = settings.get_ui_by_section_and_key(section, key)
743 ui = settings.get_ui_by_section_and_key(section, key)
744 result_key = self._get_form_ui_key(section, key)
744 result_key = self._get_form_ui_key(section, key)
745
745
746 if ui:
746 if ui:
747 if section in ('hooks', 'extensions'):
747 if section in ('hooks', 'extensions'):
748 result[result_key] = ui.ui_active
748 result[result_key] = ui.ui_active
749 elif result_key in ['vcs_git_lfs_enabled']:
749 elif result_key in ['vcs_git_lfs_enabled']:
750 result[result_key] = ui.ui_active
750 result[result_key] = ui.ui_active
751 else:
751 else:
752 result[result_key] = ui.ui_value
752 result[result_key] = ui.ui_value
753
753
754 for name in self.GENERAL_SETTINGS:
754 for name in self.GENERAL_SETTINGS:
755 setting = settings.get_setting_by_name(name)
755 setting = settings.get_setting_by_name(name)
756 if setting:
756 if setting:
757 result_key = 'rhodecode_{}'.format(name)
757 result_key = 'rhodecode_{}'.format(name)
758 result[result_key] = setting.app_settings_value
758 result[result_key] = setting.app_settings_value
759
759
760 return result
760 return result
761
761
762 def _get_form_ui_key(self, section, key):
762 def _get_form_ui_key(self, section, key):
763 return '{section}_{key}'.format(
763 return '{section}_{key}'.format(
764 section=section, key=key.replace('.', '_'))
764 section=section, key=key.replace('.', '_'))
765
765
766 def _create_or_update_ui(
766 def _create_or_update_ui(
767 self, settings, section, key, value=None, active=None):
767 self, settings, section, key, value=None, active=None):
768 ui = settings.get_ui_by_section_and_key(section, key)
768 ui = settings.get_ui_by_section_and_key(section, key)
769 if not ui:
769 if not ui:
770 active = True if active is None else active
770 active = True if active is None else active
771 settings.create_ui_section_value(
771 settings.create_ui_section_value(
772 section, value, key=key, active=active)
772 section, value, key=key, active=active)
773 else:
773 else:
774 if active is not None:
774 if active is not None:
775 ui.ui_active = active
775 ui.ui_active = active
776 if value is not None:
776 if value is not None:
777 ui.ui_value = value
777 ui.ui_value = value
778 Session().add(ui)
778 Session().add(ui)
779
779
780 def _create_svn_settings(self, settings, data):
780 def _create_svn_settings(self, settings, data):
781 svn_settings = {
781 svn_settings = {
782 'new_svn_branch': self.SVN_BRANCH_SECTION,
782 'new_svn_branch': self.SVN_BRANCH_SECTION,
783 'new_svn_tag': self.SVN_TAG_SECTION
783 'new_svn_tag': self.SVN_TAG_SECTION
784 }
784 }
785 for key in svn_settings:
785 for key in svn_settings:
786 if data.get(key):
786 if data.get(key):
787 settings.create_ui_section_value(svn_settings[key], data[key])
787 settings.create_ui_section_value(svn_settings[key], data[key])
788
788
789 def _create_or_update_general_settings(self, settings, data):
789 def _create_or_update_general_settings(self, settings, data):
790 for name in self.GENERAL_SETTINGS:
790 for name in self.GENERAL_SETTINGS:
791 data_key = 'rhodecode_{}'.format(name)
791 data_key = 'rhodecode_{}'.format(name)
792 if data_key not in data:
792 if data_key not in data:
793 raise ValueError(
793 raise ValueError(
794 'The given data does not contain {} key'.format(data_key))
794 'The given data does not contain {} key'.format(data_key))
795 setting = settings.create_or_update_setting(
795 setting = settings.create_or_update_setting(
796 name, data[data_key], 'bool')
796 name, data[data_key], 'bool')
797 Session().add(setting)
797 Session().add(setting)
798
798
799 def _get_settings_keys(self, settings, data):
799 def _get_settings_keys(self, settings, data):
800 data_keys = [self._get_form_ui_key(*s) for s in settings]
800 data_keys = [self._get_form_ui_key(*s) for s in settings]
801 for data_key in data_keys:
801 for data_key in data_keys:
802 if data_key not in data:
802 if data_key not in data:
803 raise ValueError(
803 raise ValueError(
804 'The given data does not contain {} key'.format(data_key))
804 'The given data does not contain {} key'.format(data_key))
805 return data_keys
805 return data_keys
806
806
807 def create_largeobjects_dirs_if_needed(self, repo_store_path):
807 def create_largeobjects_dirs_if_needed(self, repo_store_path):
808 """
808 """
809 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
809 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
810 does a repository scan if enabled in the settings.
810 does a repository scan if enabled in the settings.
811 """
811 """
812
812
813 from rhodecode.lib.vcs.backends.hg import largefiles_store
813 from rhodecode.lib.vcs.backends.hg import largefiles_store
814 from rhodecode.lib.vcs.backends.git import lfs_store
814 from rhodecode.lib.vcs.backends.git import lfs_store
815
815
816 paths = [
816 paths = [
817 largefiles_store(repo_store_path),
817 largefiles_store(repo_store_path),
818 lfs_store(repo_store_path)]
818 lfs_store(repo_store_path)]
819
819
820 for path in paths:
820 for path in paths:
821 if os.path.isdir(path):
821 if os.path.isdir(path):
822 continue
822 continue
823 if os.path.isfile(path):
823 if os.path.isfile(path):
824 continue
824 continue
825 # not a file nor dir, we try to create it
825 # not a file nor dir, we try to create it
826 try:
826 try:
827 os.makedirs(path)
827 os.makedirs(path)
828 except Exception:
828 except Exception:
829 log.warning('Failed to create largefiles dir:%s', path)
829 log.warning('Failed to create largefiles dir:%s', path)
@@ -1,108 +1,108 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2018 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
22
23 import pytest
23 import pytest
24
24
25 from rhodecode.lib import rc_cache
25 from rhodecode.lib import rc_cache
26
26
27
27
28 @pytest.mark.usefixtures( 'app')
28 @pytest.mark.usefixtures( 'app')
29 class TestCaches(object):
29 class TestCaches(object):
30
30
31 def test_cache_decorator_init_not_configured(self):
31 def test_cache_decorator_init_not_configured(self):
32 with pytest.raises(EnvironmentError):
32 with pytest.raises(EnvironmentError):
33 rc_cache.get_or_create_region('dontexist')
33 rc_cache.get_or_create_region('dontexist')
34
34
35 @pytest.mark.parametrize('region_name', [
35 @pytest.mark.parametrize('region_name', [
36 'cache_perms', u'cache_perms',
36 'cache_perms', u'cache_perms',
37 ])
37 ])
38 def test_cache_decorator_init(self, region_name):
38 def test_cache_decorator_init(self, region_name):
39 namespace = region_name
39 namespace = region_name
40 cache_region = rc_cache.get_or_create_region(
40 cache_region = rc_cache.get_or_create_region(
41 region_name, region_namespace=namespace)
41 region_name, region_namespace=namespace)
42 assert cache_region
42 assert cache_region
43
43
44 @pytest.mark.parametrize('example_input', [
44 @pytest.mark.parametrize('example_input', [
45 ('',),
45 ('',),
46 (u'/ac',),
46 (u'/ac',),
47 (u'/ac', 1, 2, object()),
47 (u'/ac', 1, 2, object()),
48 (u'/Δ™Δ‡c', 1, 2, object()),
48 (u'/Δ™Δ‡c', 1, 2, object()),
49 ('/Δ…ac',),
49 ('/Δ…ac',),
50 (u'/ac', ),
50 (u'/ac', ),
51 ])
51 ])
52 def test_cache_manager_create_key(self, example_input):
52 def test_cache_manager_create_key(self, example_input):
53 key = rc_cache.utils.compute_key_from_params(*example_input)
53 key = rc_cache.utils.compute_key_from_params(*example_input)
54 assert key
54 assert key
55
55
56 @pytest.mark.parametrize('example_namespace', [
56 @pytest.mark.parametrize('example_namespace', [
57 'namespace', None
57 'namespace', None
58 ])
58 ])
59 @pytest.mark.parametrize('example_input', [
59 @pytest.mark.parametrize('example_input', [
60 ('',),
60 ('',),
61 (u'/ac',),
61 (u'/ac',),
62 (u'/ac', 1, 2, object()),
62 (u'/ac', 1, 2, object()),
63 (u'/Δ™Δ‡c', 1, 2, object()),
63 (u'/Δ™Δ‡c', 1, 2, object()),
64 ('/Δ…ac',),
64 ('/Δ…ac',),
65 (u'/ac', ),
65 (u'/ac', ),
66 ])
66 ])
67 def test_cache_keygen(self, example_input, example_namespace):
67 def test_cache_keygen(self, example_input, example_namespace):
68 def func_wrapped():
68 def func_wrapped():
69 return 1
69 return 1
70 func = rc_cache.utils.key_generator(example_namespace, func_wrapped)
70 func = rc_cache.utils.key_generator(example_namespace, func_wrapped)
71 key = func(*example_input)
71 key = func(*example_input)
72 assert key
72 assert key
73
73
74 def test_store_value_in_cache(self):
74 def test_store_value_in_cache(self):
75 cache_region = rc_cache.get_or_create_region('cache_perms')
75 cache_region = rc_cache.get_or_create_region('cache_perms')
76 # make sure we empty the cache now
76 # make sure we empty the cache now
77 cache_region.delete_multi(cache_region.backend.list_keys())
77 cache_region.delete_multi(cache_region.backend.list_keys())
78
78
79 assert cache_region.backend.list_keys() == []
79 assert cache_region.backend.list_keys() == []
80
80
81 @cache_region.cache_on_arguments(expiration_time=5)
81 @cache_region.conditional_cache_on_arguments(expiration_time=5)
82 def compute(key):
82 def compute(key):
83 return time.time()
83 return time.time()
84
84
85 for x in range(10):
85 for x in range(10):
86 compute(x)
86 compute(x)
87
87
88 assert len(set(cache_region.backend.list_keys())) == 10
88 assert len(set(cache_region.backend.list_keys())) == 10
89
89
90 def test_store_and_get_value_from_region(self):
90 def test_store_and_get_value_from_region(self):
91 cache_region = rc_cache.get_or_create_region('cache_perms')
91 cache_region = rc_cache.get_or_create_region('cache_perms')
92 # make sure we empty the cache now
92 # make sure we empty the cache now
93 for key in cache_region.backend.list_keys():
93 for key in cache_region.backend.list_keys():
94 cache_region.delete(key)
94 cache_region.delete(key)
95 assert cache_region.backend.list_keys() == []
95 assert cache_region.backend.list_keys() == []
96
96
97 @cache_region.cache_on_arguments(expiration_time=5)
97 @cache_region.conditional_cache_on_arguments(expiration_time=5)
98 def compute(key):
98 def compute(key):
99 return time.time()
99 return time.time()
100
100
101 result = set()
101 result = set()
102 for x in range(10):
102 for x in range(10):
103 ret = compute('x')
103 ret = compute('x')
104 result.add(ret)
104 result.add(ret)
105
105
106 # once computed we have only one value (the same from cache)
106 # once computed we have only one value (the same from cache)
107 # after executing it 10x
107 # after executing it 10x
108 assert len(result) == 1
108 assert len(result) == 1
General Comments 0
You need to be logged in to leave comments. Login now