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