##// END OF EJS Templates
application: not use config.scan(), and replace all @add_view decorator into a explicit add_view call for faster app start.
milka -
r4610:1c249462 stable
parent child Browse files
Show More

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

@@ -1,555 +1,564 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 sys
24 24 import types
25 25 import fnmatch
26 26
27 27 import decorator
28 28 import venusian
29 29 from collections import OrderedDict
30 30
31 31 from pyramid.exceptions import ConfigurationError
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34 from pyramid.httpexceptions import HTTPNotFound
35 35
36 36 from rhodecode.api.exc import (
37 37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.lib.auth import AuthUser
40 40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
41 41 from rhodecode.lib.exc_tracking import store_exception
42 42 from rhodecode.lib.ext_json import json
43 43 from rhodecode.lib.utils2 import safe_str
44 44 from rhodecode.lib.plugins.utils import get_plugin_settings
45 45 from rhodecode.model.db import User, UserApiKeys
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49 DEFAULT_RENDERER = 'jsonrpc_renderer'
50 50 DEFAULT_URL = '/_admin/apiv2'
51 51
52 52
53 53 def find_methods(jsonrpc_methods, pattern):
54 54 matches = OrderedDict()
55 55 if not isinstance(pattern, (list, tuple)):
56 56 pattern = [pattern]
57 57
58 58 for single_pattern in pattern:
59 59 for method_name, method in jsonrpc_methods.items():
60 60 if fnmatch.fnmatch(method_name, single_pattern):
61 61 matches[method_name] = method
62 62 return matches
63 63
64 64
65 65 class ExtJsonRenderer(object):
66 66 """
67 67 Custom renderer that mkaes use of our ext_json lib
68 68
69 69 """
70 70
71 71 def __init__(self, serializer=json.dumps, **kw):
72 72 """ Any keyword arguments will be passed to the ``serializer``
73 73 function."""
74 74 self.serializer = serializer
75 75 self.kw = kw
76 76
77 77 def __call__(self, info):
78 78 """ Returns a plain JSON-encoded string with content-type
79 79 ``application/json``. The content-type may be overridden by
80 80 setting ``request.response.content_type``."""
81 81
82 82 def _render(value, system):
83 83 request = system.get('request')
84 84 if request is not None:
85 85 response = request.response
86 86 ct = response.content_type
87 87 if ct == response.default_content_type:
88 88 response.content_type = 'application/json'
89 89
90 90 return self.serializer(value, **self.kw)
91 91
92 92 return _render
93 93
94 94
95 95 def jsonrpc_response(request, result):
96 96 rpc_id = getattr(request, 'rpc_id', None)
97 97 response = request.response
98 98
99 99 # store content_type before render is called
100 100 ct = response.content_type
101 101
102 102 ret_value = ''
103 103 if rpc_id:
104 104 ret_value = {
105 105 'id': rpc_id,
106 106 'result': result,
107 107 'error': None,
108 108 }
109 109
110 110 # fetch deprecation warnings, and store it inside results
111 111 deprecation = getattr(request, 'rpc_deprecation', None)
112 112 if deprecation:
113 113 ret_value['DEPRECATION_WARNING'] = deprecation
114 114
115 115 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
116 116 response.body = safe_str(raw_body, response.charset)
117 117
118 118 if ct == response.default_content_type:
119 119 response.content_type = 'application/json'
120 120
121 121 return response
122 122
123 123
124 124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
125 125 """
126 126 Generate a Response object with a JSON-RPC error body
127 127
128 128 :param code:
129 129 :param retid:
130 130 :param message:
131 131 """
132 132 err_dict = {'id': retid, 'result': None, 'error': message}
133 133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
134 134
135 135 return Response(
136 136 body=body,
137 137 status=code,
138 138 content_type='application/json',
139 139 headerlist=headers
140 140 )
141 141
142 142
143 143 def exception_view(exc, request):
144 144 rpc_id = getattr(request, 'rpc_id', None)
145 145
146 146 if isinstance(exc, JSONRPCError):
147 147 fault_message = safe_str(exc.message)
148 148 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
149 149 elif isinstance(exc, JSONRPCValidationError):
150 150 colander_exc = exc.colander_exception
151 151 # TODO(marcink): think maybe of nicer way to serialize errors ?
152 152 fault_message = colander_exc.asdict()
153 153 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
154 154 elif isinstance(exc, JSONRPCForbidden):
155 155 fault_message = 'Access was denied to this resource.'
156 156 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
157 157 elif isinstance(exc, HTTPNotFound):
158 158 method = request.rpc_method
159 159 log.debug('json-rpc method `%s` not found in list of '
160 160 'api calls: %s, rpc_id:%s',
161 161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
162 162
163 163 similar = 'none'
164 164 try:
165 165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
166 166 similar_found = find_methods(
167 167 request.registry.jsonrpc_methods, similar_paterns)
168 168 similar = ', '.join(similar_found.keys()) or similar
169 169 except Exception:
170 170 # make the whole above block safe
171 171 pass
172 172
173 173 fault_message = "No such method: {}. Similar methods: {}".format(
174 174 method, similar)
175 175 else:
176 176 fault_message = 'undefined error'
177 177 exc_info = exc.exc_info()
178 178 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
179 179
180 180 return jsonrpc_error(request, fault_message, rpc_id)
181 181
182 182
183 183 def request_view(request):
184 184 """
185 185 Main request handling method. It handles all logic to call a specific
186 186 exposed method
187 187 """
188 188 # cython compatible inspect
189 189 from rhodecode.config.patches import inspect_getargspec
190 190 inspect = inspect_getargspec()
191 191
192 192 # check if we can find this session using api_key, get_by_auth_token
193 193 # search not expired tokens only
194 194 try:
195 195 api_user = User.get_by_auth_token(request.rpc_api_key)
196 196
197 197 if api_user is None:
198 198 return jsonrpc_error(
199 199 request, retid=request.rpc_id, message='Invalid API KEY')
200 200
201 201 if not api_user.active:
202 202 return jsonrpc_error(
203 203 request, retid=request.rpc_id,
204 204 message='Request from this user not allowed')
205 205
206 206 # check if we are allowed to use this IP
207 207 auth_u = AuthUser(
208 208 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
209 209 if not auth_u.ip_allowed:
210 210 return jsonrpc_error(
211 211 request, retid=request.rpc_id,
212 212 message='Request from IP:%s not allowed' % (
213 213 request.rpc_ip_addr,))
214 214 else:
215 215 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
216 216
217 217 # register our auth-user
218 218 request.rpc_user = auth_u
219 219 request.environ['rc_auth_user_id'] = auth_u.user_id
220 220
221 221 # now check if token is valid for API
222 222 auth_token = request.rpc_api_key
223 223 token_match = api_user.authenticate_by_token(
224 224 auth_token, roles=[UserApiKeys.ROLE_API])
225 225 invalid_token = not token_match
226 226
227 227 log.debug('Checking if API KEY is valid with proper role')
228 228 if invalid_token:
229 229 return jsonrpc_error(
230 230 request, retid=request.rpc_id,
231 231 message='API KEY invalid or, has bad role for an API call')
232 232
233 233 except Exception:
234 234 log.exception('Error on API AUTH')
235 235 return jsonrpc_error(
236 236 request, retid=request.rpc_id, message='Invalid API KEY')
237 237
238 238 method = request.rpc_method
239 239 func = request.registry.jsonrpc_methods[method]
240 240
241 241 # now that we have a method, add request._req_params to
242 242 # self.kargs and dispatch control to WGIController
243 243 argspec = inspect.getargspec(func)
244 244 arglist = argspec[0]
245 245 defaults = map(type, argspec[3] or [])
246 246 default_empty = types.NotImplementedType
247 247
248 248 # kw arguments required by this method
249 249 func_kwargs = dict(itertools.izip_longest(
250 250 reversed(arglist), reversed(defaults), fillvalue=default_empty))
251 251
252 252 # This attribute will need to be first param of a method that uses
253 253 # api_key, which is translated to instance of user at that name
254 254 user_var = 'apiuser'
255 255 request_var = 'request'
256 256
257 257 for arg in [user_var, request_var]:
258 258 if arg not in arglist:
259 259 return jsonrpc_error(
260 260 request,
261 261 retid=request.rpc_id,
262 262 message='This method [%s] does not support '
263 263 'required parameter `%s`' % (func.__name__, arg))
264 264
265 265 # get our arglist and check if we provided them as args
266 266 for arg, default in func_kwargs.items():
267 267 if arg in [user_var, request_var]:
268 268 # user_var and request_var are pre-hardcoded parameters and we
269 269 # don't need to do any translation
270 270 continue
271 271
272 272 # skip the required param check if it's default value is
273 273 # NotImplementedType (default_empty)
274 274 if default == default_empty and arg not in request.rpc_params:
275 275 return jsonrpc_error(
276 276 request,
277 277 retid=request.rpc_id,
278 278 message=('Missing non optional `%s` arg in JSON DATA' % arg)
279 279 )
280 280
281 281 # sanitize extra passed arguments
282 282 for k in request.rpc_params.keys()[:]:
283 283 if k not in func_kwargs:
284 284 del request.rpc_params[k]
285 285
286 286 call_params = request.rpc_params
287 287 call_params.update({
288 288 'request': request,
289 289 'apiuser': auth_u
290 290 })
291 291
292 292 # register some common functions for usage
293 293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
294 294
295 295 try:
296 296 ret_value = func(**call_params)
297 297 return jsonrpc_response(request, ret_value)
298 298 except JSONRPCBaseError:
299 299 raise
300 300 except Exception:
301 301 log.exception('Unhandled exception occurred on api call: %s', func)
302 302 exc_info = sys.exc_info()
303 303 exc_id, exc_type_name = store_exception(
304 304 id(exc_info), exc_info, prefix='rhodecode-api')
305 305 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
306 306 ('RhodeCode-Exception-Type', str(exc_type_name))]
307 307 return jsonrpc_error(
308 308 request, retid=request.rpc_id, message='Internal server error',
309 309 headers=error_headers)
310 310
311 311
312 312 def setup_request(request):
313 313 """
314 314 Parse a JSON-RPC request body. It's used inside the predicates method
315 315 to validate and bootstrap requests for usage in rpc calls.
316 316
317 317 We need to raise JSONRPCError here if we want to return some errors back to
318 318 user.
319 319 """
320 320
321 321 log.debug('Executing setup request: %r', request)
322 322 request.rpc_ip_addr = get_ip_addr(request.environ)
323 323 # TODO(marcink): deprecate GET at some point
324 324 if request.method not in ['POST', 'GET']:
325 325 log.debug('unsupported request method "%s"', request.method)
326 326 raise JSONRPCError(
327 327 'unsupported request method "%s". Please use POST' % request.method)
328 328
329 329 if 'CONTENT_LENGTH' not in request.environ:
330 330 log.debug("No Content-Length")
331 331 raise JSONRPCError("Empty body, No Content-Length in request")
332 332
333 333 else:
334 334 length = request.environ['CONTENT_LENGTH']
335 335 log.debug('Content-Length: %s', length)
336 336
337 337 if length == 0:
338 338 log.debug("Content-Length is 0")
339 339 raise JSONRPCError("Content-Length is 0")
340 340
341 341 raw_body = request.body
342 342 log.debug("Loading JSON body now")
343 343 try:
344 344 json_body = json.loads(raw_body)
345 345 except ValueError as e:
346 346 # catch JSON errors Here
347 347 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
348 348
349 349 request.rpc_id = json_body.get('id')
350 350 request.rpc_method = json_body.get('method')
351 351
352 352 # check required base parameters
353 353 try:
354 354 api_key = json_body.get('api_key')
355 355 if not api_key:
356 356 api_key = json_body.get('auth_token')
357 357
358 358 if not api_key:
359 359 raise KeyError('api_key or auth_token')
360 360
361 361 # TODO(marcink): support passing in token in request header
362 362
363 363 request.rpc_api_key = api_key
364 364 request.rpc_id = json_body['id']
365 365 request.rpc_method = json_body['method']
366 366 request.rpc_params = json_body['args'] \
367 367 if isinstance(json_body['args'], dict) else {}
368 368
369 369 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
370 370 except KeyError as e:
371 371 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
372 372
373 373 log.debug('setup complete, now handling method:%s rpcid:%s',
374 374 request.rpc_method, request.rpc_id, )
375 375
376 376
377 377 class RoutePredicate(object):
378 378 def __init__(self, val, config):
379 379 self.val = val
380 380
381 381 def text(self):
382 382 return 'jsonrpc route = %s' % self.val
383 383
384 384 phash = text
385 385
386 386 def __call__(self, info, request):
387 387 if self.val:
388 388 # potentially setup and bootstrap our call
389 389 setup_request(request)
390 390
391 391 # Always return True so that even if it isn't a valid RPC it
392 392 # will fall through to the underlaying handlers like notfound_view
393 393 return True
394 394
395 395
396 396 class NotFoundPredicate(object):
397 397 def __init__(self, val, config):
398 398 self.val = val
399 399 self.methods = config.registry.jsonrpc_methods
400 400
401 401 def text(self):
402 402 return 'jsonrpc method not found = {}.'.format(self.val)
403 403
404 404 phash = text
405 405
406 406 def __call__(self, info, request):
407 407 return hasattr(request, 'rpc_method')
408 408
409 409
410 410 class MethodPredicate(object):
411 411 def __init__(self, val, config):
412 412 self.method = val
413 413
414 414 def text(self):
415 415 return 'jsonrpc method = %s' % self.method
416 416
417 417 phash = text
418 418
419 419 def __call__(self, context, request):
420 420 # we need to explicitly return False here, so pyramid doesn't try to
421 421 # execute our view directly. We need our main handler to execute things
422 422 return getattr(request, 'rpc_method') == self.method
423 423
424 424
425 425 def add_jsonrpc_method(config, view, **kwargs):
426 426 # pop the method name
427 427 method = kwargs.pop('method', None)
428 428
429 429 if method is None:
430 430 raise ConfigurationError(
431 431 'Cannot register a JSON-RPC method without specifying the "method"')
432 432
433 433 # we define custom predicate, to enable to detect conflicting methods,
434 434 # those predicates are kind of "translation" from the decorator variables
435 435 # to internal predicates names
436 436
437 437 kwargs['jsonrpc_method'] = method
438 438
439 439 # register our view into global view store for validation
440 440 config.registry.jsonrpc_methods[method] = view
441 441
442 442 # we're using our main request_view handler, here, so each method
443 443 # has a unified handler for itself
444 444 config.add_view(request_view, route_name='apiv2', **kwargs)
445 445
446 446
447 447 class jsonrpc_method(object):
448 448 """
449 449 decorator that works similar to @add_view_config decorator,
450 450 but tailored for our JSON RPC
451 451 """
452 452
453 453 venusian = venusian # for testing injection
454 454
455 455 def __init__(self, method=None, **kwargs):
456 456 self.method = method
457 457 self.kwargs = kwargs
458 458
459 459 def __call__(self, wrapped):
460 460 kwargs = self.kwargs.copy()
461 461 kwargs['method'] = self.method or wrapped.__name__
462 462 depth = kwargs.pop('_depth', 0)
463 463
464 464 def callback(context, name, ob):
465 465 config = context.config.with_package(info.module)
466 466 config.add_jsonrpc_method(view=ob, **kwargs)
467 467
468 468 info = venusian.attach(wrapped, callback, category='pyramid',
469 469 depth=depth + 1)
470 470 if info.scope == 'class':
471 471 # ensure that attr is set if decorating a class method
472 472 kwargs.setdefault('attr', wrapped.__name__)
473 473
474 474 kwargs['_info'] = info.codeinfo # fbo action_method
475 475 return wrapped
476 476
477 477
478 478 class jsonrpc_deprecated_method(object):
479 479 """
480 480 Marks method as deprecated, adds log.warning, and inject special key to
481 481 the request variable to mark method as deprecated.
482 482 Also injects special docstring that extract_docs will catch to mark
483 483 method as deprecated.
484 484
485 485 :param use_method: specify which method should be used instead of
486 486 the decorated one
487 487
488 488 Use like::
489 489
490 490 @jsonrpc_method()
491 491 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
492 492 def old_func(request, apiuser, arg1, arg2):
493 493 ...
494 494 """
495 495
496 496 def __init__(self, use_method, deprecated_at_version):
497 497 self.use_method = use_method
498 498 self.deprecated_at_version = deprecated_at_version
499 499 self.deprecated_msg = ''
500 500
501 501 def __call__(self, func):
502 502 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
503 503 method=self.use_method)
504 504
505 505 docstring = """\n
506 506 .. deprecated:: {version}
507 507
508 508 {deprecation_message}
509 509
510 510 {original_docstring}
511 511 """
512 512 func.__doc__ = docstring.format(
513 513 version=self.deprecated_at_version,
514 514 deprecation_message=self.deprecated_msg,
515 515 original_docstring=func.__doc__)
516 516 return decorator.decorator(self.__wrapper, func)
517 517
518 518 def __wrapper(self, func, *fargs, **fkwargs):
519 519 log.warning('DEPRECATED API CALL on function %s, please '
520 520 'use `%s` instead', func, self.use_method)
521 521 # alter function docstring to mark as deprecated, this is picked up
522 522 # via fabric file that generates API DOC.
523 523 result = func(*fargs, **fkwargs)
524 524
525 525 request = fargs[0]
526 526 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
527 527 return result
528 528
529 529
530 def add_api_methods(config):
531 from rhodecode.api.views import (
532 deprecated_api, gist_api, pull_request_api, repo_api, repo_group_api,
533 server_api, search_api, testing_api, user_api, user_group_api)
534
535 config.scan('rhodecode.api.views')
536
537
530 538 def includeme(config):
531 539 plugin_module = 'rhodecode.api'
532 540 plugin_settings = get_plugin_settings(
533 541 plugin_module, config.registry.settings)
534 542
535 543 if not hasattr(config.registry, 'jsonrpc_methods'):
536 544 config.registry.jsonrpc_methods = OrderedDict()
537 545
538 546 # match filter by given method only
539 547 config.add_view_predicate('jsonrpc_method', MethodPredicate)
540 548 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
541 549
542 550 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
543 551 serializer=json.dumps, indent=4))
544 552 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
545 553
546 554 config.add_route_predicate(
547 555 'jsonrpc_call', RoutePredicate)
548 556
549 557 config.add_route(
550 558 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
551 559
552 config.scan(plugin_module, ignore='rhodecode.api.tests')
553 560 # register some exception handling view
554 561 config.add_view(exception_view, context=JSONRPCBaseError)
555 562 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
563
564 add_api_methods(config)
@@ -1,146 +1,148 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import logging
23 23 import collections
24 24
25 25 from zope.interface import implementer
26 26
27 27 from rhodecode.apps._base.interfaces import IAdminNavigationRegistry
28 28 from rhodecode.lib.utils2 import str2bool
29 29 from rhodecode.translation import _
30 30
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34 NavListEntry = collections.namedtuple(
35 35 'NavListEntry', ['key', 'name', 'url', 'active_list'])
36 36
37 37
38 38 class NavEntry(object):
39 39 """
40 40 Represents an entry in the admin navigation.
41 41
42 42 :param key: Unique identifier used to store reference in an OrderedDict.
43 43 :param name: Display name, usually a translation string.
44 44 :param view_name: Name of the view, used generate the URL.
45 45 :param active_list: list of urls that we select active for this element
46 46 """
47 47
48 48 def __init__(self, key, name, view_name, active_list=None):
49 49 self.key = key
50 50 self.name = name
51 51 self.view_name = view_name
52 52 self._active_list = active_list or []
53 53
54 54 def generate_url(self, request):
55 55 return request.route_path(self.view_name)
56 56
57 57 def get_localized_name(self, request):
58 58 return request.translate(self.name)
59 59
60 60 @property
61 61 def active_list(self):
62 62 active_list = [self.key]
63 63 if self._active_list:
64 64 active_list = self._active_list
65 65 return active_list
66 66
67 67
68 68 @implementer(IAdminNavigationRegistry)
69 69 class NavigationRegistry(object):
70 70
71 71 _base_entries = [
72 72 NavEntry('global', _('Global'),
73 73 'admin_settings_global'),
74 74 NavEntry('vcs', _('VCS'),
75 75 'admin_settings_vcs'),
76 76 NavEntry('visual', _('Visual'),
77 77 'admin_settings_visual'),
78 78 NavEntry('mapping', _('Remap and Rescan'),
79 79 'admin_settings_mapping'),
80 80 NavEntry('issuetracker', _('Issue Tracker'),
81 81 'admin_settings_issuetracker'),
82 82 NavEntry('email', _('Email'),
83 83 'admin_settings_email'),
84 84 NavEntry('hooks', _('Hooks'),
85 85 'admin_settings_hooks'),
86 86 NavEntry('search', _('Full Text Search'),
87 87 'admin_settings_search'),
88 88 NavEntry('system', _('System Info'),
89 89 'admin_settings_system'),
90 90 NavEntry('exceptions', _('Exceptions Tracker'),
91 91 'admin_settings_exception_tracker',
92 92 active_list=['exceptions', 'exceptions_browse']),
93 93 NavEntry('process_management', _('Processes'),
94 94 'admin_settings_process_management'),
95 95 NavEntry('sessions', _('User Sessions'),
96 96 'admin_settings_sessions'),
97 97 NavEntry('open_source', _('Open Source Licenses'),
98 98 'admin_settings_open_source'),
99 99 NavEntry('automation', _('Automation'),
100 100 'admin_settings_automation')
101 101 ]
102 102
103 103 _labs_entry = NavEntry('labs', _('Labs'),
104 104 'admin_settings_labs')
105 105
106 106 def __init__(self, labs_active=False):
107 107 self._registered_entries = collections.OrderedDict()
108 108 for item in self.__class__._base_entries:
109 109 self._registered_entries[item.key] = item
110 110
111 111 if labs_active:
112 112 self.add_entry(self._labs_entry)
113 113
114 114 def add_entry(self, entry):
115 115 self._registered_entries[entry.key] = entry
116 116
117 117 def get_navlist(self, request):
118 118 nav_list = [
119 119 NavListEntry(i.key, i.get_localized_name(request),
120 120 i.generate_url(request), i.active_list)
121 121 for i in self._registered_entries.values()]
122 122 return nav_list
123 123
124 124
125 125 def navigation_registry(request, registry=None):
126 126 """
127 127 Helper that returns the admin navigation registry.
128 128 """
129 129 pyramid_registry = registry or request.registry
130 130 nav_registry = pyramid_registry.queryUtility(IAdminNavigationRegistry)
131 131 return nav_registry
132 132
133 133
134 134 def navigation_list(request):
135 135 """
136 136 Helper that returns the admin navigation as list of NavListEntry objects.
137 137 """
138 138 return navigation_registry(request).get_navlist(request)
139 139
140 140
141 141 def includeme(config):
142 142 # Create admin navigation registry and add it to the pyramid registry.
143 143 settings = config.get_settings()
144 144 labs_active = str2bool(settings.get('labs_settings_active', False))
145 145 navigation_registry_instance = NavigationRegistry(labs_active=labs_active)
146 146 config.registry.registerUtility(navigation_registry_instance)
147 log.debug('Created new nabigation instance, %s', navigation_registry_instance)
148
This diff has been collapsed as it changes many lines, (619 lines changed) Show them Hide them
@@ -1,466 +1,1055 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def admin_routes(config):
26 26 """
27 27 Admin prefixed routes
28 28 """
29 from rhodecode.apps.admin.views.audit_logs import AdminAuditLogsView
30 from rhodecode.apps.admin.views.defaults import AdminDefaultSettingsView
31 from rhodecode.apps.admin.views.exception_tracker import ExceptionsTrackerView
32 from rhodecode.apps.admin.views.main_views import AdminMainView
33 from rhodecode.apps.admin.views.open_source_licenses import OpenSourceLicensesAdminSettingsView
34 from rhodecode.apps.admin.views.permissions import AdminPermissionsView
35 from rhodecode.apps.admin.views.process_management import AdminProcessManagementView
36 from rhodecode.apps.admin.views.repo_groups import AdminRepoGroupsView
37 from rhodecode.apps.admin.views.repositories import AdminReposView
38 from rhodecode.apps.admin.views.sessions import AdminSessionSettingsView
39 from rhodecode.apps.admin.views.settings import AdminSettingsView
40 from rhodecode.apps.admin.views.svn_config import AdminSvnConfigView
41 from rhodecode.apps.admin.views.system_info import AdminSystemInfoSettingsView
42 from rhodecode.apps.admin.views.user_groups import AdminUserGroupsView
43 from rhodecode.apps.admin.views.users import AdminUsersView, UsersView
44
29 45 config.add_route(
30 46 name='admin_audit_logs',
31 47 pattern='/audit_logs')
48 config.add_view(
49 AdminAuditLogsView,
50 attr='admin_audit_logs',
51 route_name='admin_audit_logs', request_method='GET',
52 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
32 53
33 54 config.add_route(
34 55 name='admin_audit_log_entry',
35 56 pattern='/audit_logs/{audit_log_id}')
36
37 config.add_route(
38 name='pull_requests_global_0', # backward compat
39 pattern='/pull_requests/{pull_request_id:\d+}')
40 config.add_route(
41 name='pull_requests_global_1', # backward compat
42 pattern='/pull-requests/{pull_request_id:\d+}')
43 config.add_route(
44 name='pull_requests_global',
45 pattern='/pull-request/{pull_request_id:\d+}')
57 config.add_view(
58 AdminAuditLogsView,
59 attr='admin_audit_log_entry',
60 route_name='admin_audit_log_entry', request_method='GET',
61 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
46 62
47 63 config.add_route(
48 64 name='admin_settings_open_source',
49 65 pattern='/settings/open_source')
66 config.add_view(
67 OpenSourceLicensesAdminSettingsView,
68 attr='open_source_licenses',
69 route_name='admin_settings_open_source', request_method='GET',
70 renderer='rhodecode:templates/admin/settings/settings.mako')
71
50 72 config.add_route(
51 73 name='admin_settings_vcs_svn_generate_cfg',
52 74 pattern='/settings/vcs/svn_generate_cfg')
75 config.add_view(
76 AdminSvnConfigView,
77 attr='vcs_svn_generate_config',
78 route_name='admin_settings_vcs_svn_generate_cfg',
79 request_method='POST', renderer='json')
53 80
54 81 config.add_route(
55 82 name='admin_settings_system',
56 83 pattern='/settings/system')
84 config.add_view(
85 AdminSystemInfoSettingsView,
86 attr='settings_system_info',
87 route_name='admin_settings_system', request_method='GET',
88 renderer='rhodecode:templates/admin/settings/settings.mako')
89
57 90 config.add_route(
58 91 name='admin_settings_system_update',
59 92 pattern='/settings/system/updates')
93 config.add_view(
94 AdminSystemInfoSettingsView,
95 attr='settings_system_info_check_update',
96 route_name='admin_settings_system_update', request_method='GET',
97 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
60 98
61 99 config.add_route(
62 100 name='admin_settings_exception_tracker',
63 101 pattern='/settings/exceptions')
102 config.add_view(
103 ExceptionsTrackerView,
104 attr='browse_exceptions',
105 route_name='admin_settings_exception_tracker', request_method='GET',
106 renderer='rhodecode:templates/admin/settings/settings.mako')
107
64 108 config.add_route(
65 109 name='admin_settings_exception_tracker_delete_all',
66 pattern='/settings/exceptions/delete')
110 pattern='/settings/exceptions_delete_all')
111 config.add_view(
112 ExceptionsTrackerView,
113 attr='exception_delete_all',
114 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
115 renderer='rhodecode:templates/admin/settings/settings.mako')
116
67 117 config.add_route(
68 118 name='admin_settings_exception_tracker_show',
69 119 pattern='/settings/exceptions/{exception_id}')
120 config.add_view(
121 ExceptionsTrackerView,
122 attr='exception_show',
123 route_name='admin_settings_exception_tracker_show', request_method='GET',
124 renderer='rhodecode:templates/admin/settings/settings.mako')
125
70 126 config.add_route(
71 127 name='admin_settings_exception_tracker_delete',
72 128 pattern='/settings/exceptions/{exception_id}/delete')
129 config.add_view(
130 ExceptionsTrackerView,
131 attr='exception_delete',
132 route_name='admin_settings_exception_tracker_delete', request_method='POST',
133 renderer='rhodecode:templates/admin/settings/settings.mako')
73 134
74 135 config.add_route(
75 136 name='admin_settings_sessions',
76 137 pattern='/settings/sessions')
138 config.add_view(
139 AdminSessionSettingsView,
140 attr='settings_sessions',
141 route_name='admin_settings_sessions', request_method='GET',
142 renderer='rhodecode:templates/admin/settings/settings.mako')
143
77 144 config.add_route(
78 145 name='admin_settings_sessions_cleanup',
79 146 pattern='/settings/sessions/cleanup')
147 config.add_view(
148 AdminSessionSettingsView,
149 attr='settings_sessions_cleanup',
150 route_name='admin_settings_sessions_cleanup', request_method='POST')
80 151
81 152 config.add_route(
82 153 name='admin_settings_process_management',
83 154 pattern='/settings/process_management')
155 config.add_view(
156 AdminProcessManagementView,
157 attr='process_management',
158 route_name='admin_settings_process_management', request_method='GET',
159 renderer='rhodecode:templates/admin/settings/settings.mako')
160
84 161 config.add_route(
85 162 name='admin_settings_process_management_data',
86 163 pattern='/settings/process_management/data')
164 config.add_view(
165 AdminProcessManagementView,
166 attr='process_management_data',
167 route_name='admin_settings_process_management_data', request_method='GET',
168 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
169
87 170 config.add_route(
88 171 name='admin_settings_process_management_signal',
89 172 pattern='/settings/process_management/signal')
173 config.add_view(
174 AdminProcessManagementView,
175 attr='process_management_signal',
176 route_name='admin_settings_process_management_signal',
177 request_method='POST', renderer='json_ext')
178
90 179 config.add_route(
91 180 name='admin_settings_process_management_master_signal',
92 181 pattern='/settings/process_management/master_signal')
182 config.add_view(
183 AdminProcessManagementView,
184 attr='process_management_master_signal',
185 route_name='admin_settings_process_management_master_signal',
186 request_method='POST', renderer='json_ext')
93 187
94 188 # default settings
95 189 config.add_route(
96 190 name='admin_defaults_repositories',
97 191 pattern='/defaults/repositories')
192 config.add_view(
193 AdminDefaultSettingsView,
194 attr='defaults_repository_show',
195 route_name='admin_defaults_repositories', request_method='GET',
196 renderer='rhodecode:templates/admin/defaults/defaults.mako')
197
98 198 config.add_route(
99 199 name='admin_defaults_repositories_update',
100 200 pattern='/defaults/repositories/update')
201 config.add_view(
202 AdminDefaultSettingsView,
203 attr='defaults_repository_update',
204 route_name='admin_defaults_repositories_update', request_method='POST',
205 renderer='rhodecode:templates/admin/defaults/defaults.mako')
101 206
102 207 # admin settings
103 208
104 209 config.add_route(
105 210 name='admin_settings',
106 211 pattern='/settings')
212 config.add_view(
213 AdminSettingsView,
214 attr='settings_global',
215 route_name='admin_settings', request_method='GET',
216 renderer='rhodecode:templates/admin/settings/settings.mako')
217
107 218 config.add_route(
108 219 name='admin_settings_update',
109 220 pattern='/settings/update')
221 config.add_view(
222 AdminSettingsView,
223 attr='settings_global_update',
224 route_name='admin_settings_update', request_method='POST',
225 renderer='rhodecode:templates/admin/settings/settings.mako')
110 226
111 227 config.add_route(
112 228 name='admin_settings_global',
113 229 pattern='/settings/global')
230 config.add_view(
231 AdminSettingsView,
232 attr='settings_global',
233 route_name='admin_settings_global', request_method='GET',
234 renderer='rhodecode:templates/admin/settings/settings.mako')
235
114 236 config.add_route(
115 237 name='admin_settings_global_update',
116 238 pattern='/settings/global/update')
239 config.add_view(
240 AdminSettingsView,
241 attr='settings_global_update',
242 route_name='admin_settings_global_update', request_method='POST',
243 renderer='rhodecode:templates/admin/settings/settings.mako')
117 244
118 245 config.add_route(
119 246 name='admin_settings_vcs',
120 247 pattern='/settings/vcs')
248 config.add_view(
249 AdminSettingsView,
250 attr='settings_vcs',
251 route_name='admin_settings_vcs', request_method='GET',
252 renderer='rhodecode:templates/admin/settings/settings.mako')
253
121 254 config.add_route(
122 255 name='admin_settings_vcs_update',
123 256 pattern='/settings/vcs/update')
257 config.add_view(
258 AdminSettingsView,
259 attr='settings_vcs_update',
260 route_name='admin_settings_vcs_update', request_method='POST',
261 renderer='rhodecode:templates/admin/settings/settings.mako')
262
124 263 config.add_route(
125 264 name='admin_settings_vcs_svn_pattern_delete',
126 265 pattern='/settings/vcs/svn_pattern_delete')
266 config.add_view(
267 AdminSettingsView,
268 attr='settings_vcs_delete_svn_pattern',
269 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
270 renderer='json_ext', xhr=True)
127 271
128 272 config.add_route(
129 273 name='admin_settings_mapping',
130 274 pattern='/settings/mapping')
275 config.add_view(
276 AdminSettingsView,
277 attr='settings_mapping',
278 route_name='admin_settings_mapping', request_method='GET',
279 renderer='rhodecode:templates/admin/settings/settings.mako')
280
131 281 config.add_route(
132 282 name='admin_settings_mapping_update',
133 283 pattern='/settings/mapping/update')
284 config.add_view(
285 AdminSettingsView,
286 attr='settings_mapping_update',
287 route_name='admin_settings_mapping_update', request_method='POST',
288 renderer='rhodecode:templates/admin/settings/settings.mako')
134 289
135 290 config.add_route(
136 291 name='admin_settings_visual',
137 292 pattern='/settings/visual')
293 config.add_view(
294 AdminSettingsView,
295 attr='settings_visual',
296 route_name='admin_settings_visual', request_method='GET',
297 renderer='rhodecode:templates/admin/settings/settings.mako')
298
138 299 config.add_route(
139 300 name='admin_settings_visual_update',
140 301 pattern='/settings/visual/update')
302 config.add_view(
303 AdminSettingsView,
304 attr='settings_visual_update',
305 route_name='admin_settings_visual_update', request_method='POST',
306 renderer='rhodecode:templates/admin/settings/settings.mako')
141 307
142 308 config.add_route(
143 309 name='admin_settings_issuetracker',
144 310 pattern='/settings/issue-tracker')
311 config.add_view(
312 AdminSettingsView,
313 attr='settings_issuetracker',
314 route_name='admin_settings_issuetracker', request_method='GET',
315 renderer='rhodecode:templates/admin/settings/settings.mako')
316
145 317 config.add_route(
146 318 name='admin_settings_issuetracker_update',
147 319 pattern='/settings/issue-tracker/update')
320 config.add_view(
321 AdminSettingsView,
322 attr='settings_issuetracker_update',
323 route_name='admin_settings_issuetracker_update', request_method='POST',
324 renderer='rhodecode:templates/admin/settings/settings.mako')
325
148 326 config.add_route(
149 327 name='admin_settings_issuetracker_test',
150 328 pattern='/settings/issue-tracker/test')
329 config.add_view(
330 AdminSettingsView,
331 attr='settings_issuetracker_test',
332 route_name='admin_settings_issuetracker_test', request_method='POST',
333 renderer='string', xhr=True)
334
151 335 config.add_route(
152 336 name='admin_settings_issuetracker_delete',
153 337 pattern='/settings/issue-tracker/delete')
338 config.add_view(
339 AdminSettingsView,
340 attr='settings_issuetracker_delete',
341 route_name='admin_settings_issuetracker_delete', request_method='POST',
342 renderer='json_ext', xhr=True)
154 343
155 344 config.add_route(
156 345 name='admin_settings_email',
157 346 pattern='/settings/email')
347 config.add_view(
348 AdminSettingsView,
349 attr='settings_email',
350 route_name='admin_settings_email', request_method='GET',
351 renderer='rhodecode:templates/admin/settings/settings.mako')
352
158 353 config.add_route(
159 354 name='admin_settings_email_update',
160 355 pattern='/settings/email/update')
356 config.add_view(
357 AdminSettingsView,
358 attr='settings_email_update',
359 route_name='admin_settings_email_update', request_method='POST',
360 renderer='rhodecode:templates/admin/settings/settings.mako')
161 361
162 362 config.add_route(
163 363 name='admin_settings_hooks',
164 364 pattern='/settings/hooks')
365 config.add_view(
366 AdminSettingsView,
367 attr='settings_hooks',
368 route_name='admin_settings_hooks', request_method='GET',
369 renderer='rhodecode:templates/admin/settings/settings.mako')
370
165 371 config.add_route(
166 372 name='admin_settings_hooks_update',
167 373 pattern='/settings/hooks/update')
374 config.add_view(
375 AdminSettingsView,
376 attr='settings_hooks_update',
377 route_name='admin_settings_hooks_update', request_method='POST',
378 renderer='rhodecode:templates/admin/settings/settings.mako')
379
168 380 config.add_route(
169 381 name='admin_settings_hooks_delete',
170 382 pattern='/settings/hooks/delete')
383 config.add_view(
384 AdminSettingsView,
385 attr='settings_hooks_update',
386 route_name='admin_settings_hooks_delete', request_method='POST',
387 renderer='rhodecode:templates/admin/settings/settings.mako')
171 388
172 389 config.add_route(
173 390 name='admin_settings_search',
174 391 pattern='/settings/search')
392 config.add_view(
393 AdminSettingsView,
394 attr='settings_search',
395 route_name='admin_settings_search', request_method='GET',
396 renderer='rhodecode:templates/admin/settings/settings.mako')
175 397
176 398 config.add_route(
177 399 name='admin_settings_labs',
178 400 pattern='/settings/labs')
401 config.add_view(
402 AdminSettingsView,
403 attr='settings_labs',
404 route_name='admin_settings_labs', request_method='GET',
405 renderer='rhodecode:templates/admin/settings/settings.mako')
406
179 407 config.add_route(
180 408 name='admin_settings_labs_update',
181 409 pattern='/settings/labs/update')
410 config.add_view(
411 AdminSettingsView,
412 attr='settings_labs_update',
413 route_name='admin_settings_labs_update', request_method='POST',
414 renderer='rhodecode:templates/admin/settings/settings.mako')
182 415
183 416 # Automation EE feature
184 417 config.add_route(
185 418 'admin_settings_automation',
186 419 pattern=ADMIN_PREFIX + '/settings/automation')
420 config.add_view(
421 AdminSettingsView,
422 attr='settings_automation',
423 route_name='admin_settings_automation', request_method='GET',
424 renderer='rhodecode:templates/admin/settings/settings.mako')
187 425
188 426 # global permissions
189 427
190 428 config.add_route(
191 429 name='admin_permissions_application',
192 430 pattern='/permissions/application')
431 config.add_view(
432 AdminPermissionsView,
433 attr='permissions_application',
434 route_name='admin_permissions_application', request_method='GET',
435 renderer='rhodecode:templates/admin/permissions/permissions.mako')
436
193 437 config.add_route(
194 438 name='admin_permissions_application_update',
195 439 pattern='/permissions/application/update')
440 config.add_view(
441 AdminPermissionsView,
442 attr='permissions_application_update',
443 route_name='admin_permissions_application_update', request_method='POST',
444 renderer='rhodecode:templates/admin/permissions/permissions.mako')
196 445
197 446 config.add_route(
198 447 name='admin_permissions_global',
199 448 pattern='/permissions/global')
449 config.add_view(
450 AdminPermissionsView,
451 attr='permissions_global',
452 route_name='admin_permissions_global', request_method='GET',
453 renderer='rhodecode:templates/admin/permissions/permissions.mako')
454
200 455 config.add_route(
201 456 name='admin_permissions_global_update',
202 457 pattern='/permissions/global/update')
458 config.add_view(
459 AdminPermissionsView,
460 attr='permissions_global_update',
461 route_name='admin_permissions_global_update', request_method='POST',
462 renderer='rhodecode:templates/admin/permissions/permissions.mako')
203 463
204 464 config.add_route(
205 465 name='admin_permissions_object',
206 466 pattern='/permissions/object')
467 config.add_view(
468 AdminPermissionsView,
469 attr='permissions_objects',
470 route_name='admin_permissions_object', request_method='GET',
471 renderer='rhodecode:templates/admin/permissions/permissions.mako')
472
207 473 config.add_route(
208 474 name='admin_permissions_object_update',
209 475 pattern='/permissions/object/update')
476 config.add_view(
477 AdminPermissionsView,
478 attr='permissions_objects_update',
479 route_name='admin_permissions_object_update', request_method='POST',
480 renderer='rhodecode:templates/admin/permissions/permissions.mako')
210 481
211 482 # Branch perms EE feature
212 483 config.add_route(
213 484 name='admin_permissions_branch',
214 485 pattern='/permissions/branch')
486 config.add_view(
487 AdminPermissionsView,
488 attr='permissions_branch',
489 route_name='admin_permissions_branch', request_method='GET',
490 renderer='rhodecode:templates/admin/permissions/permissions.mako')
215 491
216 492 config.add_route(
217 493 name='admin_permissions_ips',
218 494 pattern='/permissions/ips')
495 config.add_view(
496 AdminPermissionsView,
497 attr='permissions_ips',
498 route_name='admin_permissions_ips', request_method='GET',
499 renderer='rhodecode:templates/admin/permissions/permissions.mako')
219 500
220 501 config.add_route(
221 502 name='admin_permissions_overview',
222 503 pattern='/permissions/overview')
504 config.add_view(
505 AdminPermissionsView,
506 attr='permissions_overview',
507 route_name='admin_permissions_overview', request_method='GET',
508 renderer='rhodecode:templates/admin/permissions/permissions.mako')
223 509
224 510 config.add_route(
225 511 name='admin_permissions_auth_token_access',
226 512 pattern='/permissions/auth_token_access')
513 config.add_view(
514 AdminPermissionsView,
515 attr='auth_token_access',
516 route_name='admin_permissions_auth_token_access', request_method='GET',
517 renderer='rhodecode:templates/admin/permissions/permissions.mako')
227 518
228 519 config.add_route(
229 520 name='admin_permissions_ssh_keys',
230 521 pattern='/permissions/ssh_keys')
522 config.add_view(
523 AdminPermissionsView,
524 attr='ssh_keys',
525 route_name='admin_permissions_ssh_keys', request_method='GET',
526 renderer='rhodecode:templates/admin/permissions/permissions.mako')
527
231 528 config.add_route(
232 529 name='admin_permissions_ssh_keys_data',
233 530 pattern='/permissions/ssh_keys/data')
531 config.add_view(
532 AdminPermissionsView,
533 attr='ssh_keys_data',
534 route_name='admin_permissions_ssh_keys_data', request_method='GET',
535 renderer='json_ext', xhr=True)
536
234 537 config.add_route(
235 538 name='admin_permissions_ssh_keys_update',
236 539 pattern='/permissions/ssh_keys/update')
540 config.add_view(
541 AdminPermissionsView,
542 attr='ssh_keys_update',
543 route_name='admin_permissions_ssh_keys_update', request_method='POST',
544 renderer='rhodecode:templates/admin/permissions/permissions.mako')
237 545
238 546 # users admin
239 547 config.add_route(
240 548 name='users',
241 549 pattern='/users')
550 config.add_view(
551 AdminUsersView,
552 attr='users_list',
553 route_name='users', request_method='GET',
554 renderer='rhodecode:templates/admin/users/users.mako')
242 555
243 556 config.add_route(
244 557 name='users_data',
245 558 pattern='/users_data')
559 config.add_view(
560 AdminUsersView,
561 attr='users_list_data',
562 # renderer defined below
563 route_name='users_data', request_method='GET',
564 renderer='json_ext', xhr=True)
246 565
247 566 config.add_route(
248 567 name='users_create',
249 568 pattern='/users/create')
569 config.add_view(
570 AdminUsersView,
571 attr='users_create',
572 route_name='users_create', request_method='POST',
573 renderer='rhodecode:templates/admin/users/user_add.mako')
250 574
251 575 config.add_route(
252 576 name='users_new',
253 577 pattern='/users/new')
578 config.add_view(
579 AdminUsersView,
580 attr='users_new',
581 route_name='users_new', request_method='GET',
582 renderer='rhodecode:templates/admin/users/user_add.mako')
254 583
255 584 # user management
256 585 config.add_route(
257 586 name='user_edit',
258 587 pattern='/users/{user_id:\d+}/edit',
259 588 user_route=True)
589 config.add_view(
590 UsersView,
591 attr='user_edit',
592 route_name='user_edit', request_method='GET',
593 renderer='rhodecode:templates/admin/users/user_edit.mako')
594
260 595 config.add_route(
261 596 name='user_edit_advanced',
262 597 pattern='/users/{user_id:\d+}/edit/advanced',
263 598 user_route=True)
599 config.add_view(
600 UsersView,
601 attr='user_edit_advanced',
602 route_name='user_edit_advanced', request_method='GET',
603 renderer='rhodecode:templates/admin/users/user_edit.mako')
604
264 605 config.add_route(
265 606 name='user_edit_global_perms',
266 607 pattern='/users/{user_id:\d+}/edit/global_permissions',
267 608 user_route=True)
609 config.add_view(
610 UsersView,
611 attr='user_edit_global_perms',
612 route_name='user_edit_global_perms', request_method='GET',
613 renderer='rhodecode:templates/admin/users/user_edit.mako')
614
268 615 config.add_route(
269 616 name='user_edit_global_perms_update',
270 617 pattern='/users/{user_id:\d+}/edit/global_permissions/update',
271 618 user_route=True)
619 config.add_view(
620 UsersView,
621 attr='user_edit_global_perms_update',
622 route_name='user_edit_global_perms_update', request_method='POST',
623 renderer='rhodecode:templates/admin/users/user_edit.mako')
624
272 625 config.add_route(
273 626 name='user_update',
274 627 pattern='/users/{user_id:\d+}/update',
275 628 user_route=True)
629 config.add_view(
630 UsersView,
631 attr='user_update',
632 route_name='user_update', request_method='POST',
633 renderer='rhodecode:templates/admin/users/user_edit.mako')
634
276 635 config.add_route(
277 636 name='user_delete',
278 637 pattern='/users/{user_id:\d+}/delete',
279 638 user_route=True)
639 config.add_view(
640 UsersView,
641 attr='user_delete',
642 route_name='user_delete', request_method='POST',
643 renderer='rhodecode:templates/admin/users/user_edit.mako')
644
280 645 config.add_route(
281 646 name='user_enable_force_password_reset',
282 647 pattern='/users/{user_id:\d+}/password_reset_enable',
283 648 user_route=True)
649 config.add_view(
650 UsersView,
651 attr='user_enable_force_password_reset',
652 route_name='user_enable_force_password_reset', request_method='POST',
653 renderer='rhodecode:templates/admin/users/user_edit.mako')
654
284 655 config.add_route(
285 656 name='user_disable_force_password_reset',
286 657 pattern='/users/{user_id:\d+}/password_reset_disable',
287 658 user_route=True)
659 config.add_view(
660 UsersView,
661 attr='user_disable_force_password_reset',
662 route_name='user_disable_force_password_reset', request_method='POST',
663 renderer='rhodecode:templates/admin/users/user_edit.mako')
664
288 665 config.add_route(
289 666 name='user_create_personal_repo_group',
290 667 pattern='/users/{user_id:\d+}/create_repo_group',
291 668 user_route=True)
669 config.add_view(
670 UsersView,
671 attr='user_create_personal_repo_group',
672 route_name='user_create_personal_repo_group', request_method='POST',
673 renderer='rhodecode:templates/admin/users/user_edit.mako')
292 674
293 675 # user notice
294 676 config.add_route(
295 677 name='user_notice_dismiss',
296 678 pattern='/users/{user_id:\d+}/notice_dismiss',
297 679 user_route=True)
680 config.add_view(
681 UsersView,
682 attr='user_notice_dismiss',
683 route_name='user_notice_dismiss', request_method='POST',
684 renderer='json_ext', xhr=True)
298 685
299 686 # user auth tokens
300 687 config.add_route(
301 688 name='edit_user_auth_tokens',
302 689 pattern='/users/{user_id:\d+}/edit/auth_tokens',
303 690 user_route=True)
691 config.add_view(
692 UsersView,
693 attr='auth_tokens',
694 route_name='edit_user_auth_tokens', request_method='GET',
695 renderer='rhodecode:templates/admin/users/user_edit.mako')
696
304 697 config.add_route(
305 698 name='edit_user_auth_tokens_view',
306 699 pattern='/users/{user_id:\d+}/edit/auth_tokens/view',
307 700 user_route=True)
701 config.add_view(
702 UsersView,
703 attr='auth_tokens_view',
704 route_name='edit_user_auth_tokens_view', request_method='POST',
705 renderer='json_ext', xhr=True)
706
308 707 config.add_route(
309 708 name='edit_user_auth_tokens_add',
310 709 pattern='/users/{user_id:\d+}/edit/auth_tokens/new',
311 710 user_route=True)
711 config.add_view(
712 UsersView,
713 attr='auth_tokens_add',
714 route_name='edit_user_auth_tokens_add', request_method='POST')
715
312 716 config.add_route(
313 717 name='edit_user_auth_tokens_delete',
314 718 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete',
315 719 user_route=True)
720 config.add_view(
721 UsersView,
722 attr='auth_tokens_delete',
723 route_name='edit_user_auth_tokens_delete', request_method='POST')
316 724
317 725 # user ssh keys
318 726 config.add_route(
319 727 name='edit_user_ssh_keys',
320 728 pattern='/users/{user_id:\d+}/edit/ssh_keys',
321 729 user_route=True)
730 config.add_view(
731 UsersView,
732 attr='ssh_keys',
733 route_name='edit_user_ssh_keys', request_method='GET',
734 renderer='rhodecode:templates/admin/users/user_edit.mako')
735
322 736 config.add_route(
323 737 name='edit_user_ssh_keys_generate_keypair',
324 738 pattern='/users/{user_id:\d+}/edit/ssh_keys/generate',
325 739 user_route=True)
740 config.add_view(
741 UsersView,
742 attr='ssh_keys_generate_keypair',
743 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
744 renderer='rhodecode:templates/admin/users/user_edit.mako')
745
326 746 config.add_route(
327 747 name='edit_user_ssh_keys_add',
328 748 pattern='/users/{user_id:\d+}/edit/ssh_keys/new',
329 749 user_route=True)
750 config.add_view(
751 UsersView,
752 attr='ssh_keys_add',
753 route_name='edit_user_ssh_keys_add', request_method='POST')
754
330 755 config.add_route(
331 756 name='edit_user_ssh_keys_delete',
332 757 pattern='/users/{user_id:\d+}/edit/ssh_keys/delete',
333 758 user_route=True)
759 config.add_view(
760 UsersView,
761 attr='ssh_keys_delete',
762 route_name='edit_user_ssh_keys_delete', request_method='POST')
334 763
335 764 # user emails
336 765 config.add_route(
337 766 name='edit_user_emails',
338 767 pattern='/users/{user_id:\d+}/edit/emails',
339 768 user_route=True)
769 config.add_view(
770 UsersView,
771 attr='emails',
772 route_name='edit_user_emails', request_method='GET',
773 renderer='rhodecode:templates/admin/users/user_edit.mako')
774
340 775 config.add_route(
341 776 name='edit_user_emails_add',
342 777 pattern='/users/{user_id:\d+}/edit/emails/new',
343 778 user_route=True)
779 config.add_view(
780 UsersView,
781 attr='emails_add',
782 route_name='edit_user_emails_add', request_method='POST')
783
344 784 config.add_route(
345 785 name='edit_user_emails_delete',
346 786 pattern='/users/{user_id:\d+}/edit/emails/delete',
347 787 user_route=True)
788 config.add_view(
789 UsersView,
790 attr='emails_delete',
791 route_name='edit_user_emails_delete', request_method='POST')
348 792
349 793 # user IPs
350 794 config.add_route(
351 795 name='edit_user_ips',
352 796 pattern='/users/{user_id:\d+}/edit/ips',
353 797 user_route=True)
798 config.add_view(
799 UsersView,
800 attr='ips',
801 route_name='edit_user_ips', request_method='GET',
802 renderer='rhodecode:templates/admin/users/user_edit.mako')
803
354 804 config.add_route(
355 805 name='edit_user_ips_add',
356 806 pattern='/users/{user_id:\d+}/edit/ips/new',
357 807 user_route_with_default=True) # enabled for default user too
808 config.add_view(
809 UsersView,
810 attr='ips_add',
811 route_name='edit_user_ips_add', request_method='POST')
812
358 813 config.add_route(
359 814 name='edit_user_ips_delete',
360 815 pattern='/users/{user_id:\d+}/edit/ips/delete',
361 816 user_route_with_default=True) # enabled for default user too
817 config.add_view(
818 UsersView,
819 attr='ips_delete',
820 route_name='edit_user_ips_delete', request_method='POST')
362 821
363 822 # user perms
364 823 config.add_route(
365 824 name='edit_user_perms_summary',
366 825 pattern='/users/{user_id:\d+}/edit/permissions_summary',
367 826 user_route=True)
827 config.add_view(
828 UsersView,
829 attr='user_perms_summary',
830 route_name='edit_user_perms_summary', request_method='GET',
831 renderer='rhodecode:templates/admin/users/user_edit.mako')
832
368 833 config.add_route(
369 834 name='edit_user_perms_summary_json',
370 835 pattern='/users/{user_id:\d+}/edit/permissions_summary/json',
371 836 user_route=True)
837 config.add_view(
838 UsersView,
839 attr='user_perms_summary_json',
840 route_name='edit_user_perms_summary_json', request_method='GET',
841 renderer='json_ext')
372 842
373 843 # user user groups management
374 844 config.add_route(
375 845 name='edit_user_groups_management',
376 846 pattern='/users/{user_id:\d+}/edit/groups_management',
377 847 user_route=True)
848 config.add_view(
849 UsersView,
850 attr='groups_management',
851 route_name='edit_user_groups_management', request_method='GET',
852 renderer='rhodecode:templates/admin/users/user_edit.mako')
378 853
379 854 config.add_route(
380 855 name='edit_user_groups_management_updates',
381 856 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
382 857 user_route=True)
858 config.add_view(
859 UsersView,
860 attr='groups_management_updates',
861 route_name='edit_user_groups_management_updates', request_method='POST')
383 862
384 863 # user audit logs
385 864 config.add_route(
386 865 name='edit_user_audit_logs',
387 866 pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
867 config.add_view(
868 UsersView,
869 attr='user_audit_logs',
870 route_name='edit_user_audit_logs', request_method='GET',
871 renderer='rhodecode:templates/admin/users/user_edit.mako')
388 872
389 873 config.add_route(
390 874 name='edit_user_audit_logs_download',
391 875 pattern='/users/{user_id:\d+}/edit/audit/download', user_route=True)
876 config.add_view(
877 UsersView,
878 attr='user_audit_logs_download',
879 route_name='edit_user_audit_logs_download', request_method='GET',
880 renderer='string')
392 881
393 882 # user caches
394 883 config.add_route(
395 884 name='edit_user_caches',
396 885 pattern='/users/{user_id:\d+}/edit/caches',
397 886 user_route=True)
887 config.add_view(
888 UsersView,
889 attr='user_caches',
890 route_name='edit_user_caches', request_method='GET',
891 renderer='rhodecode:templates/admin/users/user_edit.mako')
892
398 893 config.add_route(
399 894 name='edit_user_caches_update',
400 895 pattern='/users/{user_id:\d+}/edit/caches/update',
401 896 user_route=True)
897 config.add_view(
898 UsersView,
899 attr='user_caches_update',
900 route_name='edit_user_caches_update', request_method='POST')
402 901
403 902 # user-groups admin
404 903 config.add_route(
405 904 name='user_groups',
406 905 pattern='/user_groups')
906 config.add_view(
907 AdminUserGroupsView,
908 attr='user_groups_list',
909 route_name='user_groups', request_method='GET',
910 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
407 911
408 912 config.add_route(
409 913 name='user_groups_data',
410 914 pattern='/user_groups_data')
915 config.add_view(
916 AdminUserGroupsView,
917 attr='user_groups_list_data',
918 route_name='user_groups_data', request_method='GET',
919 renderer='json_ext', xhr=True)
411 920
412 921 config.add_route(
413 922 name='user_groups_new',
414 923 pattern='/user_groups/new')
924 config.add_view(
925 AdminUserGroupsView,
926 attr='user_groups_new',
927 route_name='user_groups_new', request_method='GET',
928 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
415 929
416 930 config.add_route(
417 931 name='user_groups_create',
418 932 pattern='/user_groups/create')
933 config.add_view(
934 AdminUserGroupsView,
935 attr='user_groups_create',
936 route_name='user_groups_create', request_method='POST',
937 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
419 938
420 939 # repos admin
421 940 config.add_route(
422 941 name='repos',
423 942 pattern='/repos')
943 config.add_view(
944 AdminReposView,
945 attr='repository_list',
946 route_name='repos', request_method='GET',
947 renderer='rhodecode:templates/admin/repos/repos.mako')
424 948
425 949 config.add_route(
426 950 name='repos_data',
427 951 pattern='/repos_data')
952 config.add_view(
953 AdminReposView,
954 attr='repository_list_data',
955 route_name='repos_data', request_method='GET',
956 renderer='json_ext', xhr=True)
428 957
429 958 config.add_route(
430 959 name='repo_new',
431 960 pattern='/repos/new')
961 config.add_view(
962 AdminReposView,
963 attr='repository_new',
964 route_name='repo_new', request_method='GET',
965 renderer='rhodecode:templates/admin/repos/repo_add.mako')
432 966
433 967 config.add_route(
434 968 name='repo_create',
435 969 pattern='/repos/create')
970 config.add_view(
971 AdminReposView,
972 attr='repository_create',
973 route_name='repo_create', request_method='POST',
974 renderer='rhodecode:templates/admin/repos/repos.mako')
436 975
437 976 # repo groups admin
438 977 config.add_route(
439 978 name='repo_groups',
440 979 pattern='/repo_groups')
980 config.add_view(
981 AdminRepoGroupsView,
982 attr='repo_group_list',
983 route_name='repo_groups', request_method='GET',
984 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
441 985
442 986 config.add_route(
443 987 name='repo_groups_data',
444 988 pattern='/repo_groups_data')
989 config.add_view(
990 AdminRepoGroupsView,
991 attr='repo_group_list_data',
992 route_name='repo_groups_data', request_method='GET',
993 renderer='json_ext', xhr=True)
445 994
446 995 config.add_route(
447 996 name='repo_group_new',
448 997 pattern='/repo_group/new')
998 config.add_view(
999 AdminRepoGroupsView,
1000 attr='repo_group_new',
1001 route_name='repo_group_new', request_method='GET',
1002 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
449 1003
450 1004 config.add_route(
451 1005 name='repo_group_create',
452 1006 pattern='/repo_group/create')
1007 config.add_view(
1008 AdminRepoGroupsView,
1009 attr='repo_group_create',
1010 route_name='repo_group_create', request_method='POST',
1011 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
453 1012
454 1013
455 1014 def includeme(config):
456 1015 from rhodecode.apps._base.navigation import includeme as nav_includeme
1016 from rhodecode.apps.admin.views.main_views import AdminMainView
457 1017
458 1018 # Create admin navigation registry and add it to the pyramid registry.
459 1019 nav_includeme(config)
460 1020
461 1021 # main admin routes
462 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
463 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
1022 config.add_route(
1023 name='admin_home', pattern=ADMIN_PREFIX)
1024 config.add_view(
1025 AdminMainView,
1026 attr='admin_main',
1027 route_name='admin_home', request_method='GET',
1028 renderer='rhodecode:templates/admin/main.mako')
1029
1030 # pr global redirect
1031 config.add_route(
1032 name='pull_requests_global_0', # backward compat
1033 pattern=ADMIN_PREFIX + '/pull_requests/{pull_request_id:\d+}')
1034 config.add_view(
1035 AdminMainView,
1036 attr='pull_requests',
1037 route_name='pull_requests_global_0', request_method='GET')
464 1038
465 # Scan module for configuration decorators.
466 config.scan('.views', ignore='.tests')
1039 config.add_route(
1040 name='pull_requests_global_1', # backward compat
1041 pattern=ADMIN_PREFIX + '/pull-requests/{pull_request_id:\d+}')
1042 config.add_view(
1043 AdminMainView,
1044 attr='pull_requests',
1045 route_name='pull_requests_global_1', request_method='GET')
1046
1047 config.add_route(
1048 name='pull_requests_global',
1049 pattern=ADMIN_PREFIX + '/pull-request/{pull_request_id:\d+}')
1050 config.add_view(
1051 AdminMainView,
1052 attr='pull_requests',
1053 route_name='pull_requests_global', request_method='GET')
1054
1055 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
@@ -1,93 +1,87 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 from pyramid.httpexceptions import HTTPNotFound
24 from pyramid.view import view_config
25 24
26 25 from rhodecode.apps._base import BaseAppView
27 26 from rhodecode.model.db import joinedload, UserLog
28 27 from rhodecode.lib.user_log_filter import user_log_filter
29 28 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
30 29 from rhodecode.lib.utils2 import safe_int
31 30 from rhodecode.lib.helpers import SqlPage
32 31
33 32 log = logging.getLogger(__name__)
34 33
35 34
36 35 class AdminAuditLogsView(BaseAppView):
36
37 37 def load_default_context(self):
38 38 c = self._get_local_tmpl_context()
39 39 return c
40 40
41 41 @LoginRequired()
42 42 @HasPermissionAllDecorator('hg.admin')
43 @view_config(
44 route_name='admin_audit_logs', request_method='GET',
45 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
46 43 def admin_audit_logs(self):
47 44 c = self.load_default_context()
48 45
49 46 users_log = UserLog.query()\
50 47 .options(joinedload(UserLog.user))\
51 48 .options(joinedload(UserLog.repository))
52 49
53 50 # FILTERING
54 51 c.search_term = self.request.GET.get('filter')
55 52 try:
56 53 users_log = user_log_filter(users_log, c.search_term)
57 54 except Exception:
58 55 # we want this to crash for now
59 56 raise
60 57
61 58 users_log = users_log.order_by(UserLog.action_date.desc())
62 59
63 60 p = safe_int(self.request.GET.get('page', 1), 1)
64 61
65 62 def url_generator(page_num):
66 63 query_params = {
67 64 'page': page_num
68 65 }
69 66 if c.search_term:
70 67 query_params['filter'] = c.search_term
71 68 return self.request.current_route_path(_query=query_params)
72 69
73 70 c.audit_logs = SqlPage(users_log, page=p, items_per_page=10,
74 71 url_maker=url_generator)
75 72 return self._get_template_context(c)
76 73
77 74 @LoginRequired()
78 75 @HasPermissionAllDecorator('hg.admin')
79 @view_config(
80 route_name='admin_audit_log_entry', request_method='GET',
81 renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
82 76 def admin_audit_log_entry(self):
83 77 c = self.load_default_context()
84 78 audit_log_id = self.request.matchdict['audit_log_id']
85 79
86 80 c.audit_log_entry = UserLog.query()\
87 81 .options(joinedload(UserLog.user))\
88 82 .options(joinedload(UserLog.repository))\
89 83 .filter(UserLog.user_log_id == audit_log_id).scalar()
90 84 if not c.audit_log_entry:
91 85 raise HTTPNotFound()
92 86
93 87 return self._get_template_context(c)
@@ -1,111 +1,103 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 from pyramid.view import view_config
27 26 from pyramid.httpexceptions import HTTPFound
28 27 from pyramid.renderers import render
29 28 from pyramid.response import Response
30 29
31 30 from rhodecode.apps._base import BaseAppView
32 31 from rhodecode.lib.auth import (
33 32 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 33 from rhodecode.lib import helpers as h
35 34 from rhodecode.model.forms import DefaultsForm
36 35 from rhodecode.model.meta import Session
37 36 from rhodecode import BACKENDS
38 37 from rhodecode.model.settings import SettingsModel
39 38
40 39 log = logging.getLogger(__name__)
41 40
42 41
43 42 class AdminDefaultSettingsView(BaseAppView):
43
44 44 def load_default_context(self):
45 45 c = self._get_local_tmpl_context()
46
47
48 46 return c
49 47
50 48 @LoginRequired()
51 49 @HasPermissionAllDecorator('hg.admin')
52 @view_config(
53 route_name='admin_defaults_repositories', request_method='GET',
54 renderer='rhodecode:templates/admin/defaults/defaults.mako')
55 50 def defaults_repository_show(self):
56 51 c = self.load_default_context()
57 52 c.backends = BACKENDS.keys()
58 53 c.active = 'repositories'
59 54 defaults = SettingsModel().get_default_repo_settings()
60 55
61 56 data = render(
62 57 'rhodecode:templates/admin/defaults/defaults.mako',
63 58 self._get_template_context(c), self.request)
64 59 html = formencode.htmlfill.render(
65 60 data,
66 61 defaults=defaults,
67 62 encoding="UTF-8",
68 63 force_defaults=False
69 64 )
70 65 return Response(html)
71 66
72 67 @LoginRequired()
73 68 @HasPermissionAllDecorator('hg.admin')
74 69 @CSRFRequired()
75 @view_config(
76 route_name='admin_defaults_repositories_update', request_method='POST',
77 renderer='rhodecode:templates/admin/defaults/defaults.mako')
78 70 def defaults_repository_update(self):
79 71 _ = self.request.translate
80 72 c = self.load_default_context()
81 73 c.active = 'repositories'
82 74 form = DefaultsForm(self.request.translate)()
83 75
84 76 try:
85 77 form_result = form.to_python(dict(self.request.POST))
86 78 for k, v in form_result.iteritems():
87 79 setting = SettingsModel().create_or_update_setting(k, v)
88 80 Session().add(setting)
89 81 Session().commit()
90 82 h.flash(_('Default settings updated successfully'),
91 83 category='success')
92 84
93 85 except formencode.Invalid as errors:
94 86 data = render(
95 87 'rhodecode:templates/admin/defaults/defaults.mako',
96 88 self._get_template_context(c), self.request)
97 89 html = formencode.htmlfill.render(
98 90 data,
99 91 defaults=errors.value,
100 92 errors=errors.error_dict or {},
101 93 prefix_error=False,
102 94 encoding="UTF-8",
103 95 force_defaults=False
104 96 )
105 97 return Response(html)
106 98 except Exception:
107 99 log.exception('Exception in update action')
108 100 h.flash(_('Error occurred during update of default values'),
109 101 category='error')
110 102
111 103 raise HTTPFound(h.route_path('admin_defaults_repositories'))
@@ -1,174 +1,161 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2018-2020 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 import os
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25 24
26 25 from rhodecode.apps._base import BaseAppView
27 26 from rhodecode.apps._base.navigation import navigation_list
28 27 from rhodecode.lib import helpers as h
29 28 from rhodecode.lib.auth import (
30 29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
31 30 from rhodecode.lib.utils2 import time_to_utcdatetime, safe_int
32 31 from rhodecode.lib import exc_tracking
33 32
34 33 log = logging.getLogger(__name__)
35 34
36 35
37 36 class ExceptionsTrackerView(BaseAppView):
38 37 def load_default_context(self):
39 38 c = self._get_local_tmpl_context()
40 39 c.navlist = navigation_list(self.request)
41 40 return c
42 41
43 42 def count_all_exceptions(self):
44 43 exc_store_path = exc_tracking.get_exc_store()
45 44 count = 0
46 45 for fname in os.listdir(exc_store_path):
47 46 parts = fname.split('_', 2)
48 47 if not len(parts) == 3:
49 48 continue
50 49 count +=1
51 50 return count
52 51
53 52 def get_all_exceptions(self, read_metadata=False, limit=None, type_filter=None):
54 53 exc_store_path = exc_tracking.get_exc_store()
55 54 exception_list = []
56 55
57 56 def key_sorter(val):
58 57 try:
59 58 return val.split('_')[-1]
60 59 except Exception:
61 60 return 0
62 61
63 62 for fname in reversed(sorted(os.listdir(exc_store_path), key=key_sorter)):
64 63
65 64 parts = fname.split('_', 2)
66 65 if not len(parts) == 3:
67 66 continue
68 67
69 68 exc_id, app_type, exc_timestamp = parts
70 69
71 70 exc = {'exc_id': exc_id, 'app_type': app_type, 'exc_type': 'unknown',
72 71 'exc_utc_date': '', 'exc_timestamp': exc_timestamp}
73 72
74 73 if read_metadata:
75 74 full_path = os.path.join(exc_store_path, fname)
76 75 if not os.path.isfile(full_path):
77 76 continue
78 77 try:
79 78 # we can read our metadata
80 79 with open(full_path, 'rb') as f:
81 80 exc_metadata = exc_tracking.exc_unserialize(f.read())
82 81 exc.update(exc_metadata)
83 82 except Exception:
84 83 log.exception('Failed to read exc data from:{}'.format(full_path))
85 84 pass
86 85 # convert our timestamp to a date obj, for nicer representation
87 86 exc['exc_utc_date'] = time_to_utcdatetime(exc['exc_timestamp'])
88 87
89 88 type_present = exc.get('exc_type')
90 89 if type_filter:
91 90 if type_present and type_present == type_filter:
92 91 exception_list.append(exc)
93 92 else:
94 93 exception_list.append(exc)
95 94
96 95 if limit and len(exception_list) >= limit:
97 96 break
98 97 return exception_list
99 98
100 99 @LoginRequired()
101 100 @HasPermissionAllDecorator('hg.admin')
102 @view_config(
103 route_name='admin_settings_exception_tracker', request_method='GET',
104 renderer='rhodecode:templates/admin/settings/settings.mako')
105 101 def browse_exceptions(self):
106 102 _ = self.request.translate
107 103 c = self.load_default_context()
108 104 c.active = 'exceptions_browse'
109 105 c.limit = safe_int(self.request.GET.get('limit')) or 50
110 106 c.type_filter = self.request.GET.get('type_filter')
111 107 c.next_limit = c.limit + 50
112 108 c.exception_list = self.get_all_exceptions(
113 109 read_metadata=True, limit=c.limit, type_filter=c.type_filter)
114 110 c.exception_list_count = self.count_all_exceptions()
115 111 c.exception_store_dir = exc_tracking.get_exc_store()
116 112 return self._get_template_context(c)
117 113
118 114 @LoginRequired()
119 115 @HasPermissionAllDecorator('hg.admin')
120 @view_config(
121 route_name='admin_settings_exception_tracker_show', request_method='GET',
122 renderer='rhodecode:templates/admin/settings/settings.mako')
123 116 def exception_show(self):
124 117 _ = self.request.translate
125 118 c = self.load_default_context()
126 119
127 120 c.active = 'exceptions'
128 121 c.exception_id = self.request.matchdict['exception_id']
129 122 c.traceback = exc_tracking.read_exception(c.exception_id, prefix=None)
130 123 return self._get_template_context(c)
131 124
132 125 @LoginRequired()
133 126 @HasPermissionAllDecorator('hg.admin')
134 127 @CSRFRequired()
135 @view_config(
136 route_name='admin_settings_exception_tracker_delete_all', request_method='POST',
137 renderer='rhodecode:templates/admin/settings/settings.mako')
138 128 def exception_delete_all(self):
139 129 _ = self.request.translate
140 130 c = self.load_default_context()
141 131 type_filter = self.request.POST.get('type_filter')
142 132
143 133 c.active = 'exceptions'
144 134 all_exc = self.get_all_exceptions(read_metadata=bool(type_filter), type_filter=type_filter)
145 135 exc_count = 0
146 136
147 137 for exc in all_exc:
148 138 if type_filter:
149 139 if exc.get('exc_type') == type_filter:
150 140 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
151 141 exc_count += 1
152 142 else:
153 143 exc_tracking.delete_exception(exc['exc_id'], prefix=None)
154 144 exc_count += 1
155 145
156 146 h.flash(_('Removed {} Exceptions').format(exc_count), category='success')
157 147 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
158 148
159 149 @LoginRequired()
160 150 @HasPermissionAllDecorator('hg.admin')
161 151 @CSRFRequired()
162 @view_config(
163 route_name='admin_settings_exception_tracker_delete', request_method='POST',
164 renderer='rhodecode:templates/admin/settings/settings.mako')
165 152 def exception_delete(self):
166 153 _ = self.request.translate
167 154 c = self.load_default_context()
168 155
169 156 c.active = 'exceptions'
170 157 c.exception_id = self.request.matchdict['exception_id']
171 158 exc_tracking.delete_exception(c.exception_id, prefix=None)
172 159
173 160 h.flash(_('Removed Exception {}').format(c.exception_id), category='success')
174 161 raise HTTPFound(h.route_path('admin_settings_exception_tracker'))
@@ -1,79 +1,72 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
24 from pyramid.view import view_config
25 24
26 25 from rhodecode.apps._base import BaseAppView
27 26 from rhodecode.lib import helpers as h
28 27 from rhodecode.lib.auth import (LoginRequired, NotAnonymous, HasRepoPermissionAny)
29 28 from rhodecode.model.db import PullRequest
30 29
31 30
32 31 log = logging.getLogger(__name__)
33 32
34 33
35 34 class AdminMainView(BaseAppView):
36 35 def load_default_context(self):
37 36 c = self._get_local_tmpl_context()
38 37 return c
39 38
40 39 @LoginRequired()
41 40 @NotAnonymous()
42 @view_config(
43 route_name='admin_home', request_method='GET',
44 renderer='rhodecode:templates/admin/main.mako')
45 41 def admin_main(self):
46 42 c = self.load_default_context()
47 43 c.active = 'admin'
48 44
49 45 if not (c.is_super_admin or c.is_delegated_admin):
50 46 raise HTTPNotFound()
51 47
52 48 return self._get_template_context(c)
53 49
54 50 @LoginRequired()
55 @view_config(route_name='pull_requests_global_0', request_method='GET')
56 @view_config(route_name='pull_requests_global_1', request_method='GET')
57 @view_config(route_name='pull_requests_global', request_method='GET')
58 51 def pull_requests(self):
59 52 """
60 53 Global redirect for Pull Requests
61 54 pull_request_id: id of pull requests in the system
62 55 """
63 56
64 57 pull_request = PullRequest.get_or_404(
65 58 self.request.matchdict['pull_request_id'])
66 59 pull_request_id = pull_request.pull_request_id
67 60
68 61 repo_name = pull_request.target_repo.repo_name
69 62 # NOTE(marcink):
70 63 # check permissions so we don't redirect to repo that we don't have access to
71 64 # exposing it's name
72 65 target_repo_perm = HasRepoPermissionAny(
73 66 'repository.read', 'repository.write', 'repository.admin')(repo_name)
74 67 if not target_repo_perm:
75 68 raise HTTPNotFound()
76 69
77 70 raise HTTPFound(
78 71 h.route_path('pullrequest_show', repo_name=repo_name,
79 72 pull_request_id=pull_request_id))
@@ -1,51 +1,46 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 collections
22 22 import logging
23 23
24 from pyramid.view import view_config
25
26 24 from rhodecode.apps._base import BaseAppView
27 25 from rhodecode.apps._base.navigation import navigation_list
28 26 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
29 27 from rhodecode.lib.utils import read_opensource_licenses
30 28
31 29 log = logging.getLogger(__name__)
32 30
33 31
34 32 class OpenSourceLicensesAdminSettingsView(BaseAppView):
35 33
36 34 def load_default_context(self):
37 35 c = self._get_local_tmpl_context()
38 36 return c
39 37
40 38 @LoginRequired()
41 39 @HasPermissionAllDecorator('hg.admin')
42 @view_config(
43 route_name='admin_settings_open_source', request_method='GET',
44 renderer='rhodecode:templates/admin/settings/settings.mako')
45 40 def open_source_licenses(self):
46 41 c = self.load_default_context()
47 42 c.active = 'open_source'
48 43 c.navlist = navigation_list(self.request)
49 44 c.opensource_licenses = sorted(
50 45 read_opensource_licenses(), key=lambda d: d["name"])
51 46 return self._get_template_context(c)
@@ -1,519 +1,479 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 re
22 22 import logging
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import datetime
26 26 from pyramid.interfaces import IRoutesMapper
27 27
28 from pyramid.view import view_config
29 28 from pyramid.httpexceptions import HTTPFound
30 29 from pyramid.renderers import render
31 30 from pyramid.response import Response
32 31
33 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
35 34 from rhodecode import events
36 35
37 36 from rhodecode.lib import helpers as h
38 37 from rhodecode.lib.auth import (
39 38 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
40 39 from rhodecode.lib.utils2 import aslist, safe_unicode
41 40 from rhodecode.model.db import (
42 41 or_, coalesce, User, UserIpMap, UserSshKeys)
43 42 from rhodecode.model.forms import (
44 43 ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
45 44 from rhodecode.model.meta import Session
46 45 from rhodecode.model.permission import PermissionModel
47 46 from rhodecode.model.settings import SettingsModel
48 47
49 48
50 49 log = logging.getLogger(__name__)
51 50
52 51
53 52 class AdminPermissionsView(BaseAppView, DataGridAppView):
54 53 def load_default_context(self):
55 54 c = self._get_local_tmpl_context()
56 55 PermissionModel().set_global_permission_choices(
57 56 c, gettext_translator=self.request.translate)
58 57 return c
59 58
60 59 @LoginRequired()
61 60 @HasPermissionAllDecorator('hg.admin')
62 @view_config(
63 route_name='admin_permissions_application', request_method='GET',
64 renderer='rhodecode:templates/admin/permissions/permissions.mako')
65 61 def permissions_application(self):
66 62 c = self.load_default_context()
67 63 c.active = 'application'
68 64
69 65 c.user = User.get_default_user(refresh=True)
70 66
71 67 app_settings = c.rc_config
72 68
73 69 defaults = {
74 70 'anonymous': c.user.active,
75 71 'default_register_message': app_settings.get(
76 72 'rhodecode_register_message')
77 73 }
78 74 defaults.update(c.user.get_default_perms())
79 75
80 76 data = render('rhodecode:templates/admin/permissions/permissions.mako',
81 77 self._get_template_context(c), self.request)
82 78 html = formencode.htmlfill.render(
83 79 data,
84 80 defaults=defaults,
85 81 encoding="UTF-8",
86 82 force_defaults=False
87 83 )
88 84 return Response(html)
89 85
90 86 @LoginRequired()
91 87 @HasPermissionAllDecorator('hg.admin')
92 88 @CSRFRequired()
93 @view_config(
94 route_name='admin_permissions_application_update', request_method='POST',
95 renderer='rhodecode:templates/admin/permissions/permissions.mako')
96 89 def permissions_application_update(self):
97 90 _ = self.request.translate
98 91 c = self.load_default_context()
99 92 c.active = 'application'
100 93
101 94 _form = ApplicationPermissionsForm(
102 95 self.request.translate,
103 96 [x[0] for x in c.register_choices],
104 97 [x[0] for x in c.password_reset_choices],
105 98 [x[0] for x in c.extern_activate_choices])()
106 99
107 100 try:
108 101 form_result = _form.to_python(dict(self.request.POST))
109 102 form_result.update({'perm_user_name': User.DEFAULT_USER})
110 103 PermissionModel().update_application_permissions(form_result)
111 104
112 105 settings = [
113 106 ('register_message', 'default_register_message'),
114 107 ]
115 108 for setting, form_key in settings:
116 109 sett = SettingsModel().create_or_update_setting(
117 110 setting, form_result[form_key])
118 111 Session().add(sett)
119 112
120 113 Session().commit()
121 114 h.flash(_('Application permissions updated successfully'),
122 115 category='success')
123 116
124 117 except formencode.Invalid as errors:
125 118 defaults = errors.value
126 119
127 120 data = render(
128 121 'rhodecode:templates/admin/permissions/permissions.mako',
129 122 self._get_template_context(c), self.request)
130 123 html = formencode.htmlfill.render(
131 124 data,
132 125 defaults=defaults,
133 126 errors=errors.error_dict or {},
134 127 prefix_error=False,
135 128 encoding="UTF-8",
136 129 force_defaults=False
137 130 )
138 131 return Response(html)
139 132
140 133 except Exception:
141 134 log.exception("Exception during update of permissions")
142 135 h.flash(_('Error occurred during update of permissions'),
143 136 category='error')
144 137
145 138 affected_user_ids = [User.get_default_user_id()]
146 139 PermissionModel().trigger_permission_flush(affected_user_ids)
147 140
148 141 raise HTTPFound(h.route_path('admin_permissions_application'))
149 142
150 143 @LoginRequired()
151 144 @HasPermissionAllDecorator('hg.admin')
152 @view_config(
153 route_name='admin_permissions_object', request_method='GET',
154 renderer='rhodecode:templates/admin/permissions/permissions.mako')
155 145 def permissions_objects(self):
156 146 c = self.load_default_context()
157 147 c.active = 'objects'
158 148
159 149 c.user = User.get_default_user(refresh=True)
160 150 defaults = {}
161 151 defaults.update(c.user.get_default_perms())
162 152
163 153 data = render(
164 154 'rhodecode:templates/admin/permissions/permissions.mako',
165 155 self._get_template_context(c), self.request)
166 156 html = formencode.htmlfill.render(
167 157 data,
168 158 defaults=defaults,
169 159 encoding="UTF-8",
170 160 force_defaults=False
171 161 )
172 162 return Response(html)
173 163
174 164 @LoginRequired()
175 165 @HasPermissionAllDecorator('hg.admin')
176 166 @CSRFRequired()
177 @view_config(
178 route_name='admin_permissions_object_update', request_method='POST',
179 renderer='rhodecode:templates/admin/permissions/permissions.mako')
180 167 def permissions_objects_update(self):
181 168 _ = self.request.translate
182 169 c = self.load_default_context()
183 170 c.active = 'objects'
184 171
185 172 _form = ObjectPermissionsForm(
186 173 self.request.translate,
187 174 [x[0] for x in c.repo_perms_choices],
188 175 [x[0] for x in c.group_perms_choices],
189 176 [x[0] for x in c.user_group_perms_choices],
190 177 )()
191 178
192 179 try:
193 180 form_result = _form.to_python(dict(self.request.POST))
194 181 form_result.update({'perm_user_name': User.DEFAULT_USER})
195 182 PermissionModel().update_object_permissions(form_result)
196 183
197 184 Session().commit()
198 185 h.flash(_('Object permissions updated successfully'),
199 186 category='success')
200 187
201 188 except formencode.Invalid as errors:
202 189 defaults = errors.value
203 190
204 191 data = render(
205 192 'rhodecode:templates/admin/permissions/permissions.mako',
206 193 self._get_template_context(c), self.request)
207 194 html = formencode.htmlfill.render(
208 195 data,
209 196 defaults=defaults,
210 197 errors=errors.error_dict or {},
211 198 prefix_error=False,
212 199 encoding="UTF-8",
213 200 force_defaults=False
214 201 )
215 202 return Response(html)
216 203 except Exception:
217 204 log.exception("Exception during update of permissions")
218 205 h.flash(_('Error occurred during update of permissions'),
219 206 category='error')
220 207
221 208 affected_user_ids = [User.get_default_user_id()]
222 209 PermissionModel().trigger_permission_flush(affected_user_ids)
223 210
224 211 raise HTTPFound(h.route_path('admin_permissions_object'))
225 212
226 213 @LoginRequired()
227 214 @HasPermissionAllDecorator('hg.admin')
228 @view_config(
229 route_name='admin_permissions_branch', request_method='GET',
230 renderer='rhodecode:templates/admin/permissions/permissions.mako')
231 215 def permissions_branch(self):
232 216 c = self.load_default_context()
233 217 c.active = 'branch'
234 218
235 219 c.user = User.get_default_user(refresh=True)
236 220 defaults = {}
237 221 defaults.update(c.user.get_default_perms())
238 222
239 223 data = render(
240 224 'rhodecode:templates/admin/permissions/permissions.mako',
241 225 self._get_template_context(c), self.request)
242 226 html = formencode.htmlfill.render(
243 227 data,
244 228 defaults=defaults,
245 229 encoding="UTF-8",
246 230 force_defaults=False
247 231 )
248 232 return Response(html)
249 233
250 234 @LoginRequired()
251 235 @HasPermissionAllDecorator('hg.admin')
252 @view_config(
253 route_name='admin_permissions_global', request_method='GET',
254 renderer='rhodecode:templates/admin/permissions/permissions.mako')
255 236 def permissions_global(self):
256 237 c = self.load_default_context()
257 238 c.active = 'global'
258 239
259 240 c.user = User.get_default_user(refresh=True)
260 241 defaults = {}
261 242 defaults.update(c.user.get_default_perms())
262 243
263 244 data = render(
264 245 'rhodecode:templates/admin/permissions/permissions.mako',
265 246 self._get_template_context(c), self.request)
266 247 html = formencode.htmlfill.render(
267 248 data,
268 249 defaults=defaults,
269 250 encoding="UTF-8",
270 251 force_defaults=False
271 252 )
272 253 return Response(html)
273 254
274 255 @LoginRequired()
275 256 @HasPermissionAllDecorator('hg.admin')
276 257 @CSRFRequired()
277 @view_config(
278 route_name='admin_permissions_global_update', request_method='POST',
279 renderer='rhodecode:templates/admin/permissions/permissions.mako')
280 258 def permissions_global_update(self):
281 259 _ = self.request.translate
282 260 c = self.load_default_context()
283 261 c.active = 'global'
284 262
285 263 _form = UserPermissionsForm(
286 264 self.request.translate,
287 265 [x[0] for x in c.repo_create_choices],
288 266 [x[0] for x in c.repo_create_on_write_choices],
289 267 [x[0] for x in c.repo_group_create_choices],
290 268 [x[0] for x in c.user_group_create_choices],
291 269 [x[0] for x in c.fork_choices],
292 270 [x[0] for x in c.inherit_default_permission_choices])()
293 271
294 272 try:
295 273 form_result = _form.to_python(dict(self.request.POST))
296 274 form_result.update({'perm_user_name': User.DEFAULT_USER})
297 275 PermissionModel().update_user_permissions(form_result)
298 276
299 277 Session().commit()
300 278 h.flash(_('Global permissions updated successfully'),
301 279 category='success')
302 280
303 281 except formencode.Invalid as errors:
304 282 defaults = errors.value
305 283
306 284 data = render(
307 285 'rhodecode:templates/admin/permissions/permissions.mako',
308 286 self._get_template_context(c), self.request)
309 287 html = formencode.htmlfill.render(
310 288 data,
311 289 defaults=defaults,
312 290 errors=errors.error_dict or {},
313 291 prefix_error=False,
314 292 encoding="UTF-8",
315 293 force_defaults=False
316 294 )
317 295 return Response(html)
318 296 except Exception:
319 297 log.exception("Exception during update of permissions")
320 298 h.flash(_('Error occurred during update of permissions'),
321 299 category='error')
322 300
323 301 affected_user_ids = [User.get_default_user_id()]
324 302 PermissionModel().trigger_permission_flush(affected_user_ids)
325 303
326 304 raise HTTPFound(h.route_path('admin_permissions_global'))
327 305
328 306 @LoginRequired()
329 307 @HasPermissionAllDecorator('hg.admin')
330 @view_config(
331 route_name='admin_permissions_ips', request_method='GET',
332 renderer='rhodecode:templates/admin/permissions/permissions.mako')
333 308 def permissions_ips(self):
334 309 c = self.load_default_context()
335 310 c.active = 'ips'
336 311
337 312 c.user = User.get_default_user(refresh=True)
338 313 c.user_ip_map = (
339 314 UserIpMap.query().filter(UserIpMap.user == c.user).all())
340 315
341 316 return self._get_template_context(c)
342 317
343 318 @LoginRequired()
344 319 @HasPermissionAllDecorator('hg.admin')
345 @view_config(
346 route_name='admin_permissions_overview', request_method='GET',
347 renderer='rhodecode:templates/admin/permissions/permissions.mako')
348 320 def permissions_overview(self):
349 321 c = self.load_default_context()
350 322 c.active = 'perms'
351 323
352 324 c.user = User.get_default_user(refresh=True)
353 325 c.perm_user = c.user.AuthUser()
354 326 return self._get_template_context(c)
355 327
356 328 @LoginRequired()
357 329 @HasPermissionAllDecorator('hg.admin')
358 @view_config(
359 route_name='admin_permissions_auth_token_access', request_method='GET',
360 renderer='rhodecode:templates/admin/permissions/permissions.mako')
361 330 def auth_token_access(self):
362 331 from rhodecode import CONFIG
363 332
364 333 c = self.load_default_context()
365 334 c.active = 'auth_token_access'
366 335
367 336 c.user = User.get_default_user(refresh=True)
368 337 c.perm_user = c.user.AuthUser()
369 338
370 339 mapper = self.request.registry.queryUtility(IRoutesMapper)
371 340 c.view_data = []
372 341
373 342 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
374 343 introspector = self.request.registry.introspector
375 344
376 345 view_intr = {}
377 346 for view_data in introspector.get_category('views'):
378 347 intr = view_data['introspectable']
379 348
380 349 if 'route_name' in intr and intr['attr']:
381 350 view_intr[intr['route_name']] = '{}:{}'.format(
382 351 str(intr['derived_callable'].func_name), intr['attr']
383 352 )
384 353
385 354 c.whitelist_key = 'api_access_controllers_whitelist'
386 355 c.whitelist_file = CONFIG.get('__file__')
387 356 whitelist_views = aslist(
388 357 CONFIG.get(c.whitelist_key), sep=',')
389 358
390 359 for route_info in mapper.get_routes():
391 360 if not route_info.name.startswith('__'):
392 361 routepath = route_info.pattern
393 362
394 363 def replace(matchobj):
395 364 if matchobj.group(1):
396 365 return "{%s}" % matchobj.group(1).split(':')[0]
397 366 else:
398 367 return "{%s}" % matchobj.group(2)
399 368
400 369 routepath = _argument_prog.sub(replace, routepath)
401 370
402 371 if not routepath.startswith('/'):
403 372 routepath = '/' + routepath
404 373
405 374 view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
406 375 active = view_fqn in whitelist_views
407 376 c.view_data.append((route_info.name, view_fqn, routepath, active))
408 377
409 378 c.whitelist_views = whitelist_views
410 379 return self._get_template_context(c)
411 380
412 381 def ssh_enabled(self):
413 382 return self.request.registry.settings.get(
414 383 'ssh.generate_authorized_keyfile')
415 384
416 385 @LoginRequired()
417 386 @HasPermissionAllDecorator('hg.admin')
418 @view_config(
419 route_name='admin_permissions_ssh_keys', request_method='GET',
420 renderer='rhodecode:templates/admin/permissions/permissions.mako')
421 387 def ssh_keys(self):
422 388 c = self.load_default_context()
423 389 c.active = 'ssh_keys'
424 390 c.ssh_enabled = self.ssh_enabled()
425 391 return self._get_template_context(c)
426 392
427 393 @LoginRequired()
428 394 @HasPermissionAllDecorator('hg.admin')
429 @view_config(
430 route_name='admin_permissions_ssh_keys_data', request_method='GET',
431 renderer='json_ext', xhr=True)
432 395 def ssh_keys_data(self):
433 396 _ = self.request.translate
434 397 self.load_default_context()
435 398 column_map = {
436 399 'fingerprint': 'ssh_key_fingerprint',
437 400 'username': User.username
438 401 }
439 402 draw, start, limit = self._extract_chunk(self.request)
440 403 search_q, order_by, order_dir = self._extract_ordering(
441 404 self.request, column_map=column_map)
442 405
443 406 ssh_keys_data_total_count = UserSshKeys.query()\
444 407 .count()
445 408
446 409 # json generate
447 410 base_q = UserSshKeys.query().join(UserSshKeys.user)
448 411
449 412 if search_q:
450 413 like_expression = u'%{}%'.format(safe_unicode(search_q))
451 414 base_q = base_q.filter(or_(
452 415 User.username.ilike(like_expression),
453 416 UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
454 417 ))
455 418
456 419 users_data_total_filtered_count = base_q.count()
457 420
458 421 sort_col = self._get_order_col(order_by, UserSshKeys)
459 422 if sort_col:
460 423 if order_dir == 'asc':
461 424 # handle null values properly to order by NULL last
462 425 if order_by in ['created_on']:
463 426 sort_col = coalesce(sort_col, datetime.date.max)
464 427 sort_col = sort_col.asc()
465 428 else:
466 429 # handle null values properly to order by NULL last
467 430 if order_by in ['created_on']:
468 431 sort_col = coalesce(sort_col, datetime.date.min)
469 432 sort_col = sort_col.desc()
470 433
471 434 base_q = base_q.order_by(sort_col)
472 435 base_q = base_q.offset(start).limit(limit)
473 436
474 437 ssh_keys = base_q.all()
475 438
476 439 ssh_keys_data = []
477 440 for ssh_key in ssh_keys:
478 441 ssh_keys_data.append({
479 442 "username": h.gravatar_with_user(self.request, ssh_key.user.username),
480 443 "fingerprint": ssh_key.ssh_key_fingerprint,
481 444 "description": ssh_key.description,
482 445 "created_on": h.format_date(ssh_key.created_on),
483 446 "accessed_on": h.format_date(ssh_key.accessed_on),
484 447 "action": h.link_to(
485 448 _('Edit'), h.route_path('edit_user_ssh_keys',
486 449 user_id=ssh_key.user.user_id))
487 450 })
488 451
489 452 data = ({
490 453 'draw': draw,
491 454 'data': ssh_keys_data,
492 455 'recordsTotal': ssh_keys_data_total_count,
493 456 'recordsFiltered': users_data_total_filtered_count,
494 457 })
495 458
496 459 return data
497 460
498 461 @LoginRequired()
499 462 @HasPermissionAllDecorator('hg.admin')
500 463 @CSRFRequired()
501 @view_config(
502 route_name='admin_permissions_ssh_keys_update', request_method='POST',
503 renderer='rhodecode:templates/admin/permissions/permissions.mako')
504 464 def ssh_keys_update(self):
505 465 _ = self.request.translate
506 466 self.load_default_context()
507 467
508 468 ssh_enabled = self.ssh_enabled()
509 469 key_file = self.request.registry.settings.get(
510 470 'ssh.authorized_keys_file_path')
511 471 if ssh_enabled:
512 472 events.trigger(SshKeyFileChangeEvent(), self.request.registry)
513 473 h.flash(_('Updated SSH keys file: {}').format(key_file),
514 474 category='success')
515 475 else:
516 476 h.flash(_('SSH key support is disabled in .ini file'),
517 477 category='warning')
518 478
519 479 raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
@@ -1,182 +1,170 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 import psutil
24 24 import signal
25 from pyramid.view import view_config
25
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.apps._base.navigation import navigation_list
29 29 from rhodecode.lib import system_info
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
32 32 from rhodecode.lib.utils2 import safe_int, StrictAttributeDict
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 class AdminProcessManagementView(BaseAppView):
38 38 def load_default_context(self):
39 39 c = self._get_local_tmpl_context()
40 40 return c
41 41
42 42 def _format_proc(self, proc, with_children=False):
43 43 try:
44 44 mem = proc.memory_info()
45 45 proc_formatted = StrictAttributeDict({
46 46 'pid': proc.pid,
47 47 'name': proc.name(),
48 48 'mem_rss': mem.rss,
49 49 'mem_vms': mem.vms,
50 50 'cpu_percent': proc.cpu_percent(interval=0.1),
51 51 'create_time': proc.create_time(),
52 52 'cmd': ' '.join(proc.cmdline()),
53 53 })
54 54
55 55 if with_children:
56 56 proc_formatted.update({
57 57 'children': [self._format_proc(x)
58 58 for x in proc.children(recursive=True)]
59 59 })
60 60 except Exception:
61 61 log.exception('Failed to load proc')
62 62 proc_formatted = None
63 63 return proc_formatted
64 64
65 65 def get_processes(self):
66 66 proc_list = []
67 67 for p in psutil.process_iter():
68 68 if 'gunicorn' in p.name():
69 69 proc = self._format_proc(p, with_children=True)
70 70 if proc:
71 71 proc_list.append(proc)
72 72
73 73 return proc_list
74 74
75 75 def get_workers(self):
76 76 workers = None
77 77 try:
78 78 rc_config = system_info.rhodecode_config().value['config']
79 79 workers = rc_config['server:main'].get('workers')
80 80 except Exception:
81 81 pass
82 82
83 83 return workers or '?'
84 84
85 85 @LoginRequired()
86 86 @HasPermissionAllDecorator('hg.admin')
87 @view_config(
88 route_name='admin_settings_process_management', request_method='GET',
89 renderer='rhodecode:templates/admin/settings/settings.mako')
90 87 def process_management(self):
91 88 _ = self.request.translate
92 89 c = self.load_default_context()
93 90
94 91 c.active = 'process_management'
95 92 c.navlist = navigation_list(self.request)
96 93 c.gunicorn_processes = self.get_processes()
97 94 c.gunicorn_workers = self.get_workers()
98 95 return self._get_template_context(c)
99 96
100 97 @LoginRequired()
101 98 @HasPermissionAllDecorator('hg.admin')
102 @view_config(
103 route_name='admin_settings_process_management_data', request_method='GET',
104 renderer='rhodecode:templates/admin/settings/settings_process_management_data.mako')
105 99 def process_management_data(self):
106 100 _ = self.request.translate
107 101 c = self.load_default_context()
108 102 c.gunicorn_processes = self.get_processes()
109 103 return self._get_template_context(c)
110 104
111 105 @LoginRequired()
112 106 @HasPermissionAllDecorator('hg.admin')
113 107 @CSRFRequired()
114 @view_config(
115 route_name='admin_settings_process_management_signal',
116 request_method='POST', renderer='json_ext')
117 108 def process_management_signal(self):
118 109 pids = self.request.json.get('pids', [])
119 110 result = []
120 111
121 112 def on_terminate(proc):
122 113 msg = "terminated"
123 114 result.append(msg)
124 115
125 116 procs = []
126 117 for pid in pids:
127 118 pid = safe_int(pid)
128 119 if pid:
129 120 try:
130 121 proc = psutil.Process(pid)
131 122 except psutil.NoSuchProcess:
132 123 continue
133 124
134 125 children = proc.children(recursive=True)
135 126 if children:
136 127 log.warning('Wont kill Master Process')
137 128 else:
138 129 procs.append(proc)
139 130
140 131 for p in procs:
141 132 try:
142 133 p.terminate()
143 134 except psutil.AccessDenied as e:
144 135 log.warning('Access denied: {}'.format(e))
145 136
146 137 gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
147 138 for p in alive:
148 139 try:
149 140 p.kill()
150 141 except psutil.AccessDenied as e:
151 142 log.warning('Access denied: {}'.format(e))
152 143
153 144 return {'result': result}
154 145
155 146 @LoginRequired()
156 147 @HasPermissionAllDecorator('hg.admin')
157 148 @CSRFRequired()
158 @view_config(
159 route_name='admin_settings_process_management_master_signal',
160 request_method='POST', renderer='json_ext')
161 149 def process_management_master_signal(self):
162 150 pid_data = self.request.json.get('pid_data', {})
163 151 pid = safe_int(pid_data['pid'])
164 152 action = pid_data['action']
165 153 if pid:
166 154 try:
167 155 proc = psutil.Process(pid)
168 156 except psutil.NoSuchProcess:
169 157 return {'result': 'failure_no_such_process'}
170 158
171 159 children = proc.children(recursive=True)
172 160 if children:
173 161 # master process
174 162 if action == '+' and len(children) <= 20:
175 163 proc.send_signal(signal.SIGTTIN)
176 164 elif action == '-' and len(children) >= 2:
177 165 proc.send_signal(signal.SIGTTOU)
178 166 else:
179 167 return {'result': 'failure_wrong_action'}
180 168 return {'result': 'success'}
181 169
182 170 return {'result': 'failure_not_master'}
@@ -1,374 +1,362 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import datetime
21 21 import logging
22 22 import time
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26
27 27 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 from pyramid.view import view_config
28
29 29 from pyramid.renderers import render
30 30 from pyramid.response import Response
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import BaseAppView, DataGridAppView
34 34
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, CSRFRequired, NotAnonymous,
37 37 HasPermissionAny, HasRepoGroupPermissionAny)
38 38 from rhodecode.lib import helpers as h, audit_logger
39 39 from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
40 40 from rhodecode.model.forms import RepoGroupForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList
44 44 from rhodecode.model.db import (
45 45 or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminRepoGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54
55 55 return c
56 56
57 57 def _load_form_data(self, c):
58 58 allow_empty_group = False
59 59
60 60 if self._can_create_repo_group():
61 61 # we're global admin, we're ok and we can create TOP level groups
62 62 allow_empty_group = True
63 63
64 64 # override the choices for this form, we need to filter choices
65 65 # and display only those we have ADMIN right
66 66 groups_with_admin_rights = RepoGroupList(
67 67 RepoGroup.query().all(),
68 68 perm_set=['group.admin'], extra_kwargs=dict(user=self._rhodecode_user))
69 69 c.repo_groups = RepoGroup.groups_choices(
70 70 groups=groups_with_admin_rights,
71 71 show_empty_group=allow_empty_group)
72 72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
73 73
74 74 def _can_create_repo_group(self, parent_group_id=None):
75 75 is_admin = HasPermissionAny('hg.admin')('group create controller')
76 76 create_repo_group = HasPermissionAny(
77 77 'hg.repogroup.create.true')('group create controller')
78 78 if is_admin or (create_repo_group and not parent_group_id):
79 79 # we're global admin, or we have global repo group create
80 80 # permission
81 81 # we're ok and we can create TOP level groups
82 82 return True
83 83 elif parent_group_id:
84 84 # we check the permission if we can write to parent group
85 85 group = RepoGroup.get(parent_group_id)
86 86 group_name = group.group_name if group else None
87 87 if HasRepoGroupPermissionAny('group.admin')(
88 88 group_name, 'check if user is an admin of group'):
89 89 # we're an admin of passed in group, we're ok.
90 90 return True
91 91 else:
92 92 return False
93 93 return False
94 94
95 95 # permission check in data loading of
96 96 # `repo_group_list_data` via RepoGroupList
97 97 @LoginRequired()
98 98 @NotAnonymous()
99 @view_config(
100 route_name='repo_groups', request_method='GET',
101 renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
102 99 def repo_group_list(self):
103 100 c = self.load_default_context()
104 101 return self._get_template_context(c)
105 102
106 103 # permission check inside
107 104 @LoginRequired()
108 105 @NotAnonymous()
109 @view_config(
110 route_name='repo_groups_data', request_method='GET',
111 renderer='json_ext', xhr=True)
112 106 def repo_group_list_data(self):
113 107 self.load_default_context()
114 108 column_map = {
115 109 'name': 'group_name_hash',
116 110 'desc': 'group_description',
117 111 'last_change': 'updated_on',
118 112 'top_level_repos': 'repos_total',
119 113 'owner': 'user_username',
120 114 }
121 115 draw, start, limit = self._extract_chunk(self.request)
122 116 search_q, order_by, order_dir = self._extract_ordering(
123 117 self.request, column_map=column_map)
124 118
125 119 _render = self.request.get_partial_renderer(
126 120 'rhodecode:templates/data_table/_dt_elements.mako')
127 121 c = _render.get_call_context()
128 122
129 123 def quick_menu(repo_group_name):
130 124 return _render('quick_repo_group_menu', repo_group_name)
131 125
132 126 def repo_group_lnk(repo_group_name):
133 127 return _render('repo_group_name', repo_group_name)
134 128
135 129 def last_change(last_change):
136 130 if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
137 131 ts = time.time()
138 132 utc_offset = (datetime.datetime.fromtimestamp(ts)
139 133 - datetime.datetime.utcfromtimestamp(ts)).total_seconds()
140 134 last_change = last_change + datetime.timedelta(seconds=utc_offset)
141 135 return _render("last_change", last_change)
142 136
143 137 def desc(desc, personal):
144 138 return _render(
145 139 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
146 140
147 141 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
148 142 return _render(
149 143 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
150 144
151 145 def user_profile(username):
152 146 return _render('user_profile', username)
153 147
154 148 _perms = ['group.admin']
155 149 allowed_ids = [-1] + self._rhodecode_user.repo_group_acl_ids_from_stack(_perms)
156 150
157 151 repo_groups_data_total_count = RepoGroup.query()\
158 152 .filter(or_(
159 153 # generate multiple IN to fix limitation problems
160 154 *in_filter_generator(RepoGroup.group_id, allowed_ids)
161 155 )) \
162 156 .count()
163 157
164 158 repo_groups_data_total_inactive_count = RepoGroup.query()\
165 159 .filter(RepoGroup.group_id.in_(allowed_ids))\
166 160 .count()
167 161
168 162 repo_count = count(Repository.repo_id)
169 163 base_q = Session.query(
170 164 RepoGroup.group_name,
171 165 RepoGroup.group_name_hash,
172 166 RepoGroup.group_description,
173 167 RepoGroup.group_id,
174 168 RepoGroup.personal,
175 169 RepoGroup.updated_on,
176 170 User,
177 171 repo_count.label('repos_count')
178 172 ) \
179 173 .filter(or_(
180 174 # generate multiple IN to fix limitation problems
181 175 *in_filter_generator(RepoGroup.group_id, allowed_ids)
182 176 )) \
183 177 .outerjoin(Repository, Repository.group_id == RepoGroup.group_id) \
184 178 .join(User, User.user_id == RepoGroup.user_id) \
185 179 .group_by(RepoGroup, User)
186 180
187 181 if search_q:
188 182 like_expression = u'%{}%'.format(safe_unicode(search_q))
189 183 base_q = base_q.filter(or_(
190 184 RepoGroup.group_name.ilike(like_expression),
191 185 ))
192 186
193 187 repo_groups_data_total_filtered_count = base_q.count()
194 188 # the inactive isn't really used, but we still make it same as other data grids
195 189 # which use inactive (users,user groups)
196 190 repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
197 191
198 192 sort_defined = False
199 193 if order_by == 'group_name':
200 194 sort_col = func.lower(RepoGroup.group_name)
201 195 sort_defined = True
202 196 elif order_by == 'repos_total':
203 197 sort_col = repo_count
204 198 sort_defined = True
205 199 elif order_by == 'user_username':
206 200 sort_col = User.username
207 201 else:
208 202 sort_col = getattr(RepoGroup, order_by, None)
209 203
210 204 if sort_defined or sort_col:
211 205 if order_dir == 'asc':
212 206 sort_col = sort_col.asc()
213 207 else:
214 208 sort_col = sort_col.desc()
215 209
216 210 base_q = base_q.order_by(sort_col)
217 211 base_q = base_q.offset(start).limit(limit)
218 212
219 213 # authenticated access to user groups
220 214 auth_repo_group_list = base_q.all()
221 215
222 216 repo_groups_data = []
223 217 for repo_gr in auth_repo_group_list:
224 218 row = {
225 219 "menu": quick_menu(repo_gr.group_name),
226 220 "name": repo_group_lnk(repo_gr.group_name),
227 221
228 222 "last_change": last_change(repo_gr.updated_on),
229 223
230 224 "last_changeset": "",
231 225 "last_changeset_raw": "",
232 226
233 227 "desc": desc(repo_gr.group_description, repo_gr.personal),
234 228 "owner": user_profile(repo_gr.User.username),
235 229 "top_level_repos": repo_gr.repos_count,
236 230 "action": repo_group_actions(
237 231 repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
238 232
239 233 }
240 234
241 235 repo_groups_data.append(row)
242 236
243 237 data = ({
244 238 'draw': draw,
245 239 'data': repo_groups_data,
246 240 'recordsTotal': repo_groups_data_total_count,
247 241 'recordsTotalInactive': repo_groups_data_total_inactive_count,
248 242 'recordsFiltered': repo_groups_data_total_filtered_count,
249 243 'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
250 244 })
251 245
252 246 return data
253 247
254 248 @LoginRequired()
255 249 @NotAnonymous()
256 250 # perm checks inside
257 @view_config(
258 route_name='repo_group_new', request_method='GET',
259 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
260 251 def repo_group_new(self):
261 252 c = self.load_default_context()
262 253
263 254 # perm check for admin, create_group perm or admin of parent_group
264 255 parent_group_id = safe_int(self.request.GET.get('parent_group'))
265 256 _gr = RepoGroup.get(parent_group_id)
266 257 if not self._can_create_repo_group(parent_group_id):
267 258 raise HTTPForbidden()
268 259
269 260 self._load_form_data(c)
270 261
271 262 defaults = {} # Future proof for default of repo group
272 263
273 264 parent_group_choice = '-1'
274 265 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
275 266 parent_group_choice = self._rhodecode_user.personal_repo_group
276 267
277 268 if parent_group_id and _gr:
278 269 if parent_group_id in [x[0] for x in c.repo_groups]:
279 270 parent_group_choice = safe_unicode(parent_group_id)
280 271
281 272 defaults.update({'group_parent_id': parent_group_choice})
282 273
283 274 data = render(
284 275 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
285 276 self._get_template_context(c), self.request)
286 277
287 278 html = formencode.htmlfill.render(
288 279 data,
289 280 defaults=defaults,
290 281 encoding="UTF-8",
291 282 force_defaults=False
292 283 )
293 284 return Response(html)
294 285
295 286 @LoginRequired()
296 287 @NotAnonymous()
297 288 @CSRFRequired()
298 289 # perm checks inside
299 @view_config(
300 route_name='repo_group_create', request_method='POST',
301 renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
302 290 def repo_group_create(self):
303 291 c = self.load_default_context()
304 292 _ = self.request.translate
305 293
306 294 parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
307 295 can_create = self._can_create_repo_group(parent_group_id)
308 296
309 297 self._load_form_data(c)
310 298 # permissions for can create group based on parent_id are checked
311 299 # here in the Form
312 300 available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
313 301 repo_group_form = RepoGroupForm(
314 302 self.request.translate, available_groups=available_groups,
315 303 can_create_in_root=can_create)()
316 304
317 305 repo_group_name = self.request.POST.get('group_name')
318 306 try:
319 307 owner = self._rhodecode_user
320 308 form_result = repo_group_form.to_python(dict(self.request.POST))
321 309 copy_permissions = form_result.get('group_copy_permissions')
322 310 repo_group = RepoGroupModel().create(
323 311 group_name=form_result['group_name_full'],
324 312 group_description=form_result['group_description'],
325 313 owner=owner.user_id,
326 314 copy_permissions=form_result['group_copy_permissions']
327 315 )
328 316 Session().flush()
329 317
330 318 repo_group_data = repo_group.get_api_data()
331 319 audit_logger.store_web(
332 320 'repo_group.create', action_data={'data': repo_group_data},
333 321 user=self._rhodecode_user)
334 322
335 323 Session().commit()
336 324
337 325 _new_group_name = form_result['group_name_full']
338 326
339 327 repo_group_url = h.link_to(
340 328 _new_group_name,
341 329 h.route_path('repo_group_home', repo_group_name=_new_group_name))
342 330 h.flash(h.literal(_('Created repository group %s')
343 331 % repo_group_url), category='success')
344 332
345 333 except formencode.Invalid as errors:
346 334 data = render(
347 335 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
348 336 self._get_template_context(c), self.request)
349 337 html = formencode.htmlfill.render(
350 338 data,
351 339 defaults=errors.value,
352 340 errors=errors.error_dict or {},
353 341 prefix_error=False,
354 342 encoding="UTF-8",
355 343 force_defaults=False
356 344 )
357 345 return Response(html)
358 346 except Exception:
359 347 log.exception("Exception during creation of repository group")
360 348 h.flash(_('Error occurred during creation of repository group %s')
361 349 % repo_group_name, category='error')
362 350 raise HTTPFound(h.route_path('home'))
363 351
364 352 affected_user_ids = [self._rhodecode_user.user_id]
365 353 if copy_permissions:
366 354 user_group_perms = repo_group.permissions(expand_from_user_groups=True)
367 355 copy_perms = [perm['user_id'] for perm in user_group_perms]
368 356 # also include those newly created by copy
369 357 affected_user_ids.extend(copy_perms)
370 358 PermissionModel().trigger_permission_flush(affected_user_ids)
371 359
372 360 raise HTTPFound(
373 361 h.route_path('repo_group_home',
374 362 repo_group_name=form_result['group_name_full']))
@@ -1,266 +1,253 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 formencode
23 23 import formencode.htmlfill
24 24
25 25 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
26 from pyramid.view import view_config
26
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode import events
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.celerylib.utils import get_task_id
33 33
34 34 from rhodecode.lib.auth import (
35 35 LoginRequired, CSRFRequired, NotAnonymous,
36 36 HasPermissionAny, HasRepoGroupPermissionAny)
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib.utils import repo_name_slug
39 39 from rhodecode.lib.utils2 import safe_int, safe_unicode
40 40 from rhodecode.model.forms import RepoForm
41 41 from rhodecode.model.permission import PermissionModel
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
44 44 from rhodecode.model.settings import SettingsModel
45 45 from rhodecode.model.db import (
46 46 in_filter_generator, or_, func, Session, Repository, RepoGroup, User)
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50
51 51 class AdminReposView(BaseAppView, DataGridAppView):
52 52
53 53 def load_default_context(self):
54 54 c = self._get_local_tmpl_context()
55
56 55 return c
57 56
58 57 def _load_form_data(self, c):
59 58 acl_groups = RepoGroupList(RepoGroup.query().all(),
60 59 perm_set=['group.write', 'group.admin'])
61 60 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
62 61 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
63 62 c.personal_repo_group = self._rhodecode_user.personal_repo_group
64 63
65 64 @LoginRequired()
66 65 @NotAnonymous()
67 66 # perms check inside
68 @view_config(
69 route_name='repos', request_method='GET',
70 renderer='rhodecode:templates/admin/repos/repos.mako')
71 67 def repository_list(self):
72 68 c = self.load_default_context()
73 69 return self._get_template_context(c)
74 70
75 71 @LoginRequired()
76 72 @NotAnonymous()
77 73 # perms check inside
78 @view_config(
79 route_name='repos_data', request_method='GET',
80 renderer='json_ext', xhr=True)
81 74 def repository_list_data(self):
82 75 self.load_default_context()
83 76 column_map = {
84 77 'name': 'repo_name',
85 78 'desc': 'description',
86 79 'last_change': 'updated_on',
87 80 'owner': 'user_username',
88 81 }
89 82 draw, start, limit = self._extract_chunk(self.request)
90 83 search_q, order_by, order_dir = self._extract_ordering(
91 84 self.request, column_map=column_map)
92 85
93 86 _perms = ['repository.admin']
94 87 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(_perms)
95 88
96 89 repos_data_total_count = Repository.query() \
97 90 .filter(or_(
98 91 # generate multiple IN to fix limitation problems
99 92 *in_filter_generator(Repository.repo_id, allowed_ids))
100 93 ) \
101 94 .count()
102 95
103 96 base_q = Session.query(
104 97 Repository.repo_id,
105 98 Repository.repo_name,
106 99 Repository.description,
107 100 Repository.repo_type,
108 101 Repository.repo_state,
109 102 Repository.private,
110 103 Repository.archived,
111 104 Repository.fork,
112 105 Repository.updated_on,
113 106 Repository._changeset_cache,
114 107 User,
115 108 ) \
116 109 .filter(or_(
117 110 # generate multiple IN to fix limitation problems
118 111 *in_filter_generator(Repository.repo_id, allowed_ids))
119 112 ) \
120 113 .join(User, User.user_id == Repository.user_id) \
121 114 .group_by(Repository, User)
122 115
123 116 if search_q:
124 117 like_expression = u'%{}%'.format(safe_unicode(search_q))
125 118 base_q = base_q.filter(or_(
126 119 Repository.repo_name.ilike(like_expression),
127 120 ))
128 121
129 122 repos_data_total_filtered_count = base_q.count()
130 123
131 124 sort_defined = False
132 125 if order_by == 'repo_name':
133 126 sort_col = func.lower(Repository.repo_name)
134 127 sort_defined = True
135 128 elif order_by == 'user_username':
136 129 sort_col = User.username
137 130 else:
138 131 sort_col = getattr(Repository, order_by, None)
139 132
140 133 if sort_defined or sort_col:
141 134 if order_dir == 'asc':
142 135 sort_col = sort_col.asc()
143 136 else:
144 137 sort_col = sort_col.desc()
145 138
146 139 base_q = base_q.order_by(sort_col)
147 140 base_q = base_q.offset(start).limit(limit)
148 141
149 142 repos_list = base_q.all()
150 143
151 144 repos_data = RepoModel().get_repos_as_dict(
152 145 repo_list=repos_list, admin=True, super_user_actions=True)
153 146
154 147 data = ({
155 148 'draw': draw,
156 149 'data': repos_data,
157 150 'recordsTotal': repos_data_total_count,
158 151 'recordsFiltered': repos_data_total_filtered_count,
159 152 })
160 153 return data
161 154
162 155 @LoginRequired()
163 156 @NotAnonymous()
164 157 # perms check inside
165 @view_config(
166 route_name='repo_new', request_method='GET',
167 renderer='rhodecode:templates/admin/repos/repo_add.mako')
168 158 def repository_new(self):
169 159 c = self.load_default_context()
170 160
171 161 new_repo = self.request.GET.get('repo', '')
172 162 parent_group_id = safe_int(self.request.GET.get('parent_group'))
173 163 _gr = RepoGroup.get(parent_group_id)
174 164
175 165 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
176 166 # you're not super admin nor have global create permissions,
177 167 # but maybe you have at least write permission to a parent group ?
178 168
179 169 gr_name = _gr.group_name if _gr else None
180 170 # create repositories with write permission on group is set to true
181 171 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
182 172 group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
183 173 group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
184 174 if not (group_admin or (group_write and create_on_write)):
185 175 raise HTTPForbidden()
186 176
187 177 self._load_form_data(c)
188 178 c.new_repo = repo_name_slug(new_repo)
189 179
190 180 # apply the defaults from defaults page
191 181 defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
192 182 # set checkbox to autochecked
193 183 defaults['repo_copy_permissions'] = True
194 184
195 185 parent_group_choice = '-1'
196 186 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
197 187 parent_group_choice = self._rhodecode_user.personal_repo_group
198 188
199 189 if parent_group_id and _gr:
200 190 if parent_group_id in [x[0] for x in c.repo_groups]:
201 191 parent_group_choice = safe_unicode(parent_group_id)
202 192
203 193 defaults.update({'repo_group': parent_group_choice})
204 194
205 195 data = render('rhodecode:templates/admin/repos/repo_add.mako',
206 196 self._get_template_context(c), self.request)
207 197 html = formencode.htmlfill.render(
208 198 data,
209 199 defaults=defaults,
210 200 encoding="UTF-8",
211 201 force_defaults=False
212 202 )
213 203 return Response(html)
214 204
215 205 @LoginRequired()
216 206 @NotAnonymous()
217 207 @CSRFRequired()
218 208 # perms check inside
219 @view_config(
220 route_name='repo_create', request_method='POST',
221 renderer='rhodecode:templates/admin/repos/repos.mako')
222 209 def repository_create(self):
223 210 c = self.load_default_context()
224 211
225 212 form_result = {}
226 213 self._load_form_data(c)
227 214
228 215 try:
229 216 # CanWriteToGroup validators checks permissions of this POST
230 217 form = RepoForm(
231 218 self.request.translate, repo_groups=c.repo_groups_choices)()
232 219 form_result = form.to_python(dict(self.request.POST))
233 220 copy_permissions = form_result.get('repo_copy_permissions')
234 221 # create is done sometimes async on celery, db transaction
235 222 # management is handled there.
236 223 task = RepoModel().create(form_result, self._rhodecode_user.user_id)
237 224 task_id = get_task_id(task)
238 225 except formencode.Invalid as errors:
239 226 data = render('rhodecode:templates/admin/repos/repo_add.mako',
240 227 self._get_template_context(c), self.request)
241 228 html = formencode.htmlfill.render(
242 229 data,
243 230 defaults=errors.value,
244 231 errors=errors.error_dict or {},
245 232 prefix_error=False,
246 233 encoding="UTF-8",
247 234 force_defaults=False
248 235 )
249 236 return Response(html)
250 237
251 238 except Exception as e:
252 239 msg = self._log_creation_exception(e, form_result.get('repo_name'))
253 240 h.flash(msg, category='error')
254 241 raise HTTPFound(h.route_path('home'))
255 242
256 243 repo_name = form_result.get('repo_name_full')
257 244
258 245 affected_user_ids = [self._rhodecode_user.user_id]
259 246 if copy_permissions:
260 247 # permission flush is done in repo creating
261 248 pass
262 249 PermissionModel().trigger_permission_flush(affected_user_ids)
263 250
264 251 raise HTTPFound(
265 252 h.route_path('repo_creating', repo_name=repo_name,
266 253 _query=dict(task_id=task_id)))
@@ -1,101 +1,95 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 from pyramid.view import view_config
23
24 24 from pyramid.httpexceptions import HTTPFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps._base.navigation import navigation_list
28 28 from rhodecode.lib.auth import (
29 29 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
30 30 from rhodecode.lib.utils2 import safe_int
31 31 from rhodecode.lib import system_info
32 32 from rhodecode.lib import user_sessions
33 33 from rhodecode.lib import helpers as h
34 34
35 35
36 36 log = logging.getLogger(__name__)
37 37
38 38
39 39 class AdminSessionSettingsView(BaseAppView):
40
40 41 def load_default_context(self):
41 42 c = self._get_local_tmpl_context()
42
43
44 43 return c
45 44
46 45 @LoginRequired()
47 46 @HasPermissionAllDecorator('hg.admin')
48 @view_config(
49 route_name='admin_settings_sessions', request_method='GET',
50 renderer='rhodecode:templates/admin/settings/settings.mako')
51 47 def settings_sessions(self):
52 48 c = self.load_default_context()
53 49
54 50 c.active = 'sessions'
55 51 c.navlist = navigation_list(self.request)
56 52
57 53 c.cleanup_older_days = 60
58 54 older_than_seconds = 60 * 60 * 24 * c.cleanup_older_days
59 55
60 56 config = system_info.rhodecode_config().get_value()['value']['config']
61 57 c.session_model = user_sessions.get_session_handler(
62 58 config.get('beaker.session.type', 'memory'))(config)
63 59
64 60 c.session_conf = c.session_model.config
65 61 c.session_count = c.session_model.get_count()
66 62 c.session_expired_count = c.session_model.get_expired_count(
67 63 older_than_seconds)
68 64
69 65 return self._get_template_context(c)
70 66
71 67 @LoginRequired()
72 68 @HasPermissionAllDecorator('hg.admin')
73 69 @CSRFRequired()
74 @view_config(
75 route_name='admin_settings_sessions_cleanup', request_method='POST')
76 70 def settings_sessions_cleanup(self):
77 71 _ = self.request.translate
78 72 expire_days = safe_int(self.request.params.get('expire_days'))
79 73
80 74 if expire_days is None:
81 75 expire_days = 60
82 76
83 77 older_than_seconds = 60 * 60 * 24 * expire_days
84 78
85 79 config = system_info.rhodecode_config().get_value()['value']['config']
86 80 session_model = user_sessions.get_session_handler(
87 81 config.get('beaker.session.type', 'memory'))(config)
88 82
89 83 try:
90 84 session_model.clean_sessions(
91 85 older_than_seconds=older_than_seconds)
92 86 h.flash(_('Cleaned up old sessions'), category='success')
93 87 except user_sessions.CleanupCommand as msg:
94 88 h.flash(msg.message, category='warning')
95 89 except Exception as e:
96 90 log.exception('Failed session cleanup')
97 91 h.flash(_('Failed to cleanup up old sessions'), category='error')
98 92
99 93 redirect_to = self.request.resource_path(
100 94 self.context, route_name='admin_settings_sessions')
101 95 return HTTPFound(redirect_to)
@@ -1,792 +1,719 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 import logging
23 23 import collections
24 24
25 25 import datetime
26 26 import formencode
27 27 import formencode.htmlfill
28 28
29 29 import rhodecode
30 from pyramid.view import view_config
30
31 31 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 from rhodecode.apps._base import BaseAppView
36 36 from rhodecode.apps._base.navigation import navigation_list
37 37 from rhodecode.apps.svn_support.config_keys import generate_config
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
41 41 from rhodecode.lib.celerylib import tasks, run_task
42 42 from rhodecode.lib.utils import repo2db_mapper
43 43 from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict
44 44 from rhodecode.lib.index import searcher_from_config
45 45
46 46 from rhodecode.model.db import RhodeCodeUi, Repository
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 50 from rhodecode.model.permission import PermissionModel
51 51 from rhodecode.model.repo_group import RepoGroupModel
52 52
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.notification import EmailNotificationModel
55 55 from rhodecode.model.meta import Session
56 56 from rhodecode.model.settings import (
57 57 IssueTrackerSettingsModel, VcsSettingsModel, SettingNotFound,
58 58 SettingsModel)
59 59
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class AdminSettingsView(BaseAppView):
65 65
66 66 def load_default_context(self):
67 67 c = self._get_local_tmpl_context()
68 68 c.labs_active = str2bool(
69 69 rhodecode.CONFIG.get('labs_settings_active', 'true'))
70 70 c.navlist = navigation_list(self.request)
71
72 71 return c
73 72
74 73 @classmethod
75 74 def _get_ui_settings(cls):
76 75 ret = RhodeCodeUi.query().all()
77 76
78 77 if not ret:
79 78 raise Exception('Could not get application ui settings !')
80 79 settings = {}
81 80 for each in ret:
82 81 k = each.ui_key
83 82 v = each.ui_value
84 83 if k == '/':
85 84 k = 'root_path'
86 85
87 86 if k in ['push_ssl', 'publish', 'enabled']:
88 87 v = str2bool(v)
89 88
90 89 if k.find('.') != -1:
91 90 k = k.replace('.', '_')
92 91
93 92 if each.ui_section in ['hooks', 'extensions']:
94 93 v = each.ui_active
95 94
96 95 settings[each.ui_section + '_' + k] = v
97 96 return settings
98 97
99 98 @classmethod
100 99 def _form_defaults(cls):
101 100 defaults = SettingsModel().get_all_settings()
102 101 defaults.update(cls._get_ui_settings())
103 102
104 103 defaults.update({
105 104 'new_svn_branch': '',
106 105 'new_svn_tag': '',
107 106 })
108 107 return defaults
109 108
110 109 @LoginRequired()
111 110 @HasPermissionAllDecorator('hg.admin')
112 @view_config(
113 route_name='admin_settings_vcs', request_method='GET',
114 renderer='rhodecode:templates/admin/settings/settings.mako')
115 111 def settings_vcs(self):
116 112 c = self.load_default_context()
117 113 c.active = 'vcs'
118 114 model = VcsSettingsModel()
119 115 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
120 116 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
121 117
122 118 settings = self.request.registry.settings
123 119 c.svn_proxy_generate_config = settings[generate_config]
124 120
125 121 defaults = self._form_defaults()
126 122
127 123 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
128 124
129 125 data = render('rhodecode:templates/admin/settings/settings.mako',
130 126 self._get_template_context(c), self.request)
131 127 html = formencode.htmlfill.render(
132 128 data,
133 129 defaults=defaults,
134 130 encoding="UTF-8",
135 131 force_defaults=False
136 132 )
137 133 return Response(html)
138 134
139 135 @LoginRequired()
140 136 @HasPermissionAllDecorator('hg.admin')
141 137 @CSRFRequired()
142 @view_config(
143 route_name='admin_settings_vcs_update', request_method='POST',
144 renderer='rhodecode:templates/admin/settings/settings.mako')
145 138 def settings_vcs_update(self):
146 139 _ = self.request.translate
147 140 c = self.load_default_context()
148 141 c.active = 'vcs'
149 142
150 143 model = VcsSettingsModel()
151 144 c.svn_branch_patterns = model.get_global_svn_branch_patterns()
152 145 c.svn_tag_patterns = model.get_global_svn_tag_patterns()
153 146
154 147 settings = self.request.registry.settings
155 148 c.svn_proxy_generate_config = settings[generate_config]
156 149
157 150 application_form = ApplicationUiSettingsForm(self.request.translate)()
158 151
159 152 try:
160 153 form_result = application_form.to_python(dict(self.request.POST))
161 154 except formencode.Invalid as errors:
162 155 h.flash(
163 156 _("Some form inputs contain invalid data."),
164 157 category='error')
165 158 data = render('rhodecode:templates/admin/settings/settings.mako',
166 159 self._get_template_context(c), self.request)
167 160 html = formencode.htmlfill.render(
168 161 data,
169 162 defaults=errors.value,
170 163 errors=errors.error_dict or {},
171 164 prefix_error=False,
172 165 encoding="UTF-8",
173 166 force_defaults=False
174 167 )
175 168 return Response(html)
176 169
177 170 try:
178 171 if c.visual.allow_repo_location_change:
179 172 model.update_global_path_setting(form_result['paths_root_path'])
180 173
181 174 model.update_global_ssl_setting(form_result['web_push_ssl'])
182 175 model.update_global_hook_settings(form_result)
183 176
184 177 model.create_or_update_global_svn_settings(form_result)
185 178 model.create_or_update_global_hg_settings(form_result)
186 179 model.create_or_update_global_git_settings(form_result)
187 180 model.create_or_update_global_pr_settings(form_result)
188 181 except Exception:
189 182 log.exception("Exception while updating settings")
190 183 h.flash(_('Error occurred during updating '
191 184 'application settings'), category='error')
192 185 else:
193 186 Session().commit()
194 187 h.flash(_('Updated VCS settings'), category='success')
195 188 raise HTTPFound(h.route_path('admin_settings_vcs'))
196 189
197 190 data = render('rhodecode:templates/admin/settings/settings.mako',
198 191 self._get_template_context(c), self.request)
199 192 html = formencode.htmlfill.render(
200 193 data,
201 194 defaults=self._form_defaults(),
202 195 encoding="UTF-8",
203 196 force_defaults=False
204 197 )
205 198 return Response(html)
206 199
207 200 @LoginRequired()
208 201 @HasPermissionAllDecorator('hg.admin')
209 202 @CSRFRequired()
210 @view_config(
211 route_name='admin_settings_vcs_svn_pattern_delete', request_method='POST',
212 renderer='json_ext', xhr=True)
213 203 def settings_vcs_delete_svn_pattern(self):
214 204 delete_pattern_id = self.request.POST.get('delete_svn_pattern')
215 205 model = VcsSettingsModel()
216 206 try:
217 207 model.delete_global_svn_pattern(delete_pattern_id)
218 208 except SettingNotFound:
219 209 log.exception(
220 210 'Failed to delete svn_pattern with id %s', delete_pattern_id)
221 211 raise HTTPNotFound()
222 212
223 213 Session().commit()
224 214 return True
225 215
226 216 @LoginRequired()
227 217 @HasPermissionAllDecorator('hg.admin')
228 @view_config(
229 route_name='admin_settings_mapping', request_method='GET',
230 renderer='rhodecode:templates/admin/settings/settings.mako')
231 218 def settings_mapping(self):
232 219 c = self.load_default_context()
233 220 c.active = 'mapping'
234 221
235 222 data = render('rhodecode:templates/admin/settings/settings.mako',
236 223 self._get_template_context(c), self.request)
237 224 html = formencode.htmlfill.render(
238 225 data,
239 226 defaults=self._form_defaults(),
240 227 encoding="UTF-8",
241 228 force_defaults=False
242 229 )
243 230 return Response(html)
244 231
245 232 @LoginRequired()
246 233 @HasPermissionAllDecorator('hg.admin')
247 234 @CSRFRequired()
248 @view_config(
249 route_name='admin_settings_mapping_update', request_method='POST',
250 renderer='rhodecode:templates/admin/settings/settings.mako')
251 235 def settings_mapping_update(self):
252 236 _ = self.request.translate
253 237 c = self.load_default_context()
254 238 c.active = 'mapping'
255 239 rm_obsolete = self.request.POST.get('destroy', False)
256 240 invalidate_cache = self.request.POST.get('invalidate', False)
257 241 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
258 242
259 243 if invalidate_cache:
260 244 log.debug('invalidating all repositories cache')
261 245 for repo in Repository.get_all():
262 246 ScmModel().mark_for_invalidation(repo.repo_name, delete=True)
263 247
264 248 filesystem_repos = ScmModel().repo_scan()
265 249 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 250 PermissionModel().trigger_permission_flush()
267 251
268 252 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
269 253 h.flash(_('Repositories successfully '
270 254 'rescanned added: %s ; removed: %s') %
271 255 (_repr(added), _repr(removed)),
272 256 category='success')
273 257 raise HTTPFound(h.route_path('admin_settings_mapping'))
274 258
275 259 @LoginRequired()
276 260 @HasPermissionAllDecorator('hg.admin')
277 @view_config(
278 route_name='admin_settings', request_method='GET',
279 renderer='rhodecode:templates/admin/settings/settings.mako')
280 @view_config(
281 route_name='admin_settings_global', request_method='GET',
282 renderer='rhodecode:templates/admin/settings/settings.mako')
283 261 def settings_global(self):
284 262 c = self.load_default_context()
285 263 c.active = 'global'
286 264 c.personal_repo_group_default_pattern = RepoGroupModel()\
287 265 .get_personal_group_name_pattern()
288 266
289 267 data = render('rhodecode:templates/admin/settings/settings.mako',
290 268 self._get_template_context(c), self.request)
291 269 html = formencode.htmlfill.render(
292 270 data,
293 271 defaults=self._form_defaults(),
294 272 encoding="UTF-8",
295 273 force_defaults=False
296 274 )
297 275 return Response(html)
298 276
299 277 @LoginRequired()
300 278 @HasPermissionAllDecorator('hg.admin')
301 279 @CSRFRequired()
302 @view_config(
303 route_name='admin_settings_update', request_method='POST',
304 renderer='rhodecode:templates/admin/settings/settings.mako')
305 @view_config(
306 route_name='admin_settings_global_update', request_method='POST',
307 renderer='rhodecode:templates/admin/settings/settings.mako')
308 280 def settings_global_update(self):
309 281 _ = self.request.translate
310 282 c = self.load_default_context()
311 283 c.active = 'global'
312 284 c.personal_repo_group_default_pattern = RepoGroupModel()\
313 285 .get_personal_group_name_pattern()
314 286 application_form = ApplicationSettingsForm(self.request.translate)()
315 287 try:
316 288 form_result = application_form.to_python(dict(self.request.POST))
317 289 except formencode.Invalid as errors:
318 290 h.flash(
319 291 _("Some form inputs contain invalid data."),
320 292 category='error')
321 293 data = render('rhodecode:templates/admin/settings/settings.mako',
322 294 self._get_template_context(c), self.request)
323 295 html = formencode.htmlfill.render(
324 296 data,
325 297 defaults=errors.value,
326 298 errors=errors.error_dict or {},
327 299 prefix_error=False,
328 300 encoding="UTF-8",
329 301 force_defaults=False
330 302 )
331 303 return Response(html)
332 304
333 305 settings = [
334 306 ('title', 'rhodecode_title', 'unicode'),
335 307 ('realm', 'rhodecode_realm', 'unicode'),
336 308 ('pre_code', 'rhodecode_pre_code', 'unicode'),
337 309 ('post_code', 'rhodecode_post_code', 'unicode'),
338 310 ('captcha_public_key', 'rhodecode_captcha_public_key', 'unicode'),
339 311 ('captcha_private_key', 'rhodecode_captcha_private_key', 'unicode'),
340 312 ('create_personal_repo_group', 'rhodecode_create_personal_repo_group', 'bool'),
341 313 ('personal_repo_group_pattern', 'rhodecode_personal_repo_group_pattern', 'unicode'),
342 314 ]
343 315 try:
344 316 for setting, form_key, type_ in settings:
345 317 sett = SettingsModel().create_or_update_setting(
346 318 setting, form_result[form_key], type_)
347 319 Session().add(sett)
348 320
349 321 Session().commit()
350 322 SettingsModel().invalidate_settings_cache()
351 323 h.flash(_('Updated application settings'), category='success')
352 324 except Exception:
353 325 log.exception("Exception while updating application settings")
354 326 h.flash(
355 327 _('Error occurred during updating application settings'),
356 328 category='error')
357 329
358 330 raise HTTPFound(h.route_path('admin_settings_global'))
359 331
360 332 @LoginRequired()
361 333 @HasPermissionAllDecorator('hg.admin')
362 @view_config(
363 route_name='admin_settings_visual', request_method='GET',
364 renderer='rhodecode:templates/admin/settings/settings.mako')
365 334 def settings_visual(self):
366 335 c = self.load_default_context()
367 336 c.active = 'visual'
368 337
369 338 data = render('rhodecode:templates/admin/settings/settings.mako',
370 339 self._get_template_context(c), self.request)
371 340 html = formencode.htmlfill.render(
372 341 data,
373 342 defaults=self._form_defaults(),
374 343 encoding="UTF-8",
375 344 force_defaults=False
376 345 )
377 346 return Response(html)
378 347
379 348 @LoginRequired()
380 349 @HasPermissionAllDecorator('hg.admin')
381 350 @CSRFRequired()
382 @view_config(
383 route_name='admin_settings_visual_update', request_method='POST',
384 renderer='rhodecode:templates/admin/settings/settings.mako')
385 351 def settings_visual_update(self):
386 352 _ = self.request.translate
387 353 c = self.load_default_context()
388 354 c.active = 'visual'
389 355 application_form = ApplicationVisualisationForm(self.request.translate)()
390 356 try:
391 357 form_result = application_form.to_python(dict(self.request.POST))
392 358 except formencode.Invalid as errors:
393 359 h.flash(
394 360 _("Some form inputs contain invalid data."),
395 361 category='error')
396 362 data = render('rhodecode:templates/admin/settings/settings.mako',
397 363 self._get_template_context(c), self.request)
398 364 html = formencode.htmlfill.render(
399 365 data,
400 366 defaults=errors.value,
401 367 errors=errors.error_dict or {},
402 368 prefix_error=False,
403 369 encoding="UTF-8",
404 370 force_defaults=False
405 371 )
406 372 return Response(html)
407 373
408 374 try:
409 375 settings = [
410 376 ('show_public_icon', 'rhodecode_show_public_icon', 'bool'),
411 377 ('show_private_icon', 'rhodecode_show_private_icon', 'bool'),
412 378 ('stylify_metatags', 'rhodecode_stylify_metatags', 'bool'),
413 379 ('repository_fields', 'rhodecode_repository_fields', 'bool'),
414 380 ('dashboard_items', 'rhodecode_dashboard_items', 'int'),
415 381 ('admin_grid_items', 'rhodecode_admin_grid_items', 'int'),
416 382 ('show_version', 'rhodecode_show_version', 'bool'),
417 383 ('use_gravatar', 'rhodecode_use_gravatar', 'bool'),
418 384 ('markup_renderer', 'rhodecode_markup_renderer', 'unicode'),
419 385 ('gravatar_url', 'rhodecode_gravatar_url', 'unicode'),
420 386 ('clone_uri_tmpl', 'rhodecode_clone_uri_tmpl', 'unicode'),
421 387 ('clone_uri_ssh_tmpl', 'rhodecode_clone_uri_ssh_tmpl', 'unicode'),
422 388 ('support_url', 'rhodecode_support_url', 'unicode'),
423 389 ('show_revision_number', 'rhodecode_show_revision_number', 'bool'),
424 390 ('show_sha_length', 'rhodecode_show_sha_length', 'int'),
425 391 ]
426 392 for setting, form_key, type_ in settings:
427 393 sett = SettingsModel().create_or_update_setting(
428 394 setting, form_result[form_key], type_)
429 395 Session().add(sett)
430 396
431 397 Session().commit()
432 398 SettingsModel().invalidate_settings_cache()
433 399 h.flash(_('Updated visualisation settings'), category='success')
434 400 except Exception:
435 401 log.exception("Exception updating visualization settings")
436 402 h.flash(_('Error occurred during updating '
437 403 'visualisation settings'),
438 404 category='error')
439 405
440 406 raise HTTPFound(h.route_path('admin_settings_visual'))
441 407
442 408 @LoginRequired()
443 409 @HasPermissionAllDecorator('hg.admin')
444 @view_config(
445 route_name='admin_settings_issuetracker', request_method='GET',
446 renderer='rhodecode:templates/admin/settings/settings.mako')
447 410 def settings_issuetracker(self):
448 411 c = self.load_default_context()
449 412 c.active = 'issuetracker'
450 413 defaults = c.rc_config
451 414
452 415 entry_key = 'rhodecode_issuetracker_pat_'
453 416
454 417 c.issuetracker_entries = {}
455 418 for k, v in defaults.items():
456 419 if k.startswith(entry_key):
457 420 uid = k[len(entry_key):]
458 421 c.issuetracker_entries[uid] = None
459 422
460 423 for uid in c.issuetracker_entries:
461 424 c.issuetracker_entries[uid] = AttributeDict({
462 425 'pat': defaults.get('rhodecode_issuetracker_pat_' + uid),
463 426 'url': defaults.get('rhodecode_issuetracker_url_' + uid),
464 427 'pref': defaults.get('rhodecode_issuetracker_pref_' + uid),
465 428 'desc': defaults.get('rhodecode_issuetracker_desc_' + uid),
466 429 })
467 430
468 431 return self._get_template_context(c)
469 432
470 433 @LoginRequired()
471 434 @HasPermissionAllDecorator('hg.admin')
472 435 @CSRFRequired()
473 @view_config(
474 route_name='admin_settings_issuetracker_test', request_method='POST',
475 renderer='string', xhr=True)
476 436 def settings_issuetracker_test(self):
477 437 error_container = []
478 438
479 439 urlified_commit = h.urlify_commit_message(
480 440 self.request.POST.get('test_text', ''),
481 441 'repo_group/test_repo1', error_container=error_container)
482 442 if error_container:
483 443 def converter(inp):
484 444 return h.html_escape(unicode(inp))
485 445
486 446 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
487 447
488 448 return urlified_commit
489 449
490 450 @LoginRequired()
491 451 @HasPermissionAllDecorator('hg.admin')
492 452 @CSRFRequired()
493 @view_config(
494 route_name='admin_settings_issuetracker_update', request_method='POST',
495 renderer='rhodecode:templates/admin/settings/settings.mako')
496 453 def settings_issuetracker_update(self):
497 454 _ = self.request.translate
498 455 self.load_default_context()
499 456 settings_model = IssueTrackerSettingsModel()
500 457
501 458 try:
502 459 form = IssueTrackerPatternsForm(self.request.translate)()
503 460 data = form.to_python(self.request.POST)
504 461 except formencode.Invalid as errors:
505 462 log.exception('Failed to add new pattern')
506 463 error = errors
507 464 h.flash(_('Invalid issue tracker pattern: {}'.format(error)),
508 465 category='error')
509 466 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
510 467
511 468 if data:
512 469 for uid in data.get('delete_patterns', []):
513 470 settings_model.delete_entries(uid)
514 471
515 472 for pattern in data.get('patterns', []):
516 473 for setting, value, type_ in pattern:
517 474 sett = settings_model.create_or_update_setting(
518 475 setting, value, type_)
519 476 Session().add(sett)
520 477
521 478 Session().commit()
522 479
523 480 SettingsModel().invalidate_settings_cache()
524 481 h.flash(_('Updated issue tracker entries'), category='success')
525 482 raise HTTPFound(h.route_path('admin_settings_issuetracker'))
526 483
527 484 @LoginRequired()
528 485 @HasPermissionAllDecorator('hg.admin')
529 486 @CSRFRequired()
530 @view_config(
531 route_name='admin_settings_issuetracker_delete', request_method='POST',
532 renderer='json_ext', xhr=True)
533 487 def settings_issuetracker_delete(self):
534 488 _ = self.request.translate
535 489 self.load_default_context()
536 490 uid = self.request.POST.get('uid')
537 491 try:
538 492 IssueTrackerSettingsModel().delete_entries(uid)
539 493 except Exception:
540 494 log.exception('Failed to delete issue tracker setting %s', uid)
541 495 raise HTTPNotFound()
542 496
543 497 SettingsModel().invalidate_settings_cache()
544 498 h.flash(_('Removed issue tracker entry.'), category='success')
545 499
546 500 return {'deleted': uid}
547 501
548 502 @LoginRequired()
549 503 @HasPermissionAllDecorator('hg.admin')
550 @view_config(
551 route_name='admin_settings_email', request_method='GET',
552 renderer='rhodecode:templates/admin/settings/settings.mako')
553 504 def settings_email(self):
554 505 c = self.load_default_context()
555 506 c.active = 'email'
556 507 c.rhodecode_ini = rhodecode.CONFIG
557 508
558 509 data = render('rhodecode:templates/admin/settings/settings.mako',
559 510 self._get_template_context(c), self.request)
560 511 html = formencode.htmlfill.render(
561 512 data,
562 513 defaults=self._form_defaults(),
563 514 encoding="UTF-8",
564 515 force_defaults=False
565 516 )
566 517 return Response(html)
567 518
568 519 @LoginRequired()
569 520 @HasPermissionAllDecorator('hg.admin')
570 521 @CSRFRequired()
571 @view_config(
572 route_name='admin_settings_email_update', request_method='POST',
573 renderer='rhodecode:templates/admin/settings/settings.mako')
574 522 def settings_email_update(self):
575 523 _ = self.request.translate
576 524 c = self.load_default_context()
577 525 c.active = 'email'
578 526
579 527 test_email = self.request.POST.get('test_email')
580 528
581 529 if not test_email:
582 530 h.flash(_('Please enter email address'), category='error')
583 531 raise HTTPFound(h.route_path('admin_settings_email'))
584 532
585 533 email_kwargs = {
586 534 'date': datetime.datetime.now(),
587 535 'user': self._rhodecode_db_user
588 536 }
589 537
590 538 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
591 539 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
592 540
593 541 recipients = [test_email] if test_email else None
594 542
595 543 run_task(tasks.send_email, recipients, subject,
596 544 email_body_plaintext, email_body)
597 545
598 546 h.flash(_('Send email task created'), category='success')
599 547 raise HTTPFound(h.route_path('admin_settings_email'))
600 548
601 549 @LoginRequired()
602 550 @HasPermissionAllDecorator('hg.admin')
603 @view_config(
604 route_name='admin_settings_hooks', request_method='GET',
605 renderer='rhodecode:templates/admin/settings/settings.mako')
606 551 def settings_hooks(self):
607 552 c = self.load_default_context()
608 553 c.active = 'hooks'
609 554
610 555 model = SettingsModel()
611 556 c.hooks = model.get_builtin_hooks()
612 557 c.custom_hooks = model.get_custom_hooks()
613 558
614 559 data = render('rhodecode:templates/admin/settings/settings.mako',
615 560 self._get_template_context(c), self.request)
616 561 html = formencode.htmlfill.render(
617 562 data,
618 563 defaults=self._form_defaults(),
619 564 encoding="UTF-8",
620 565 force_defaults=False
621 566 )
622 567 return Response(html)
623 568
624 569 @LoginRequired()
625 570 @HasPermissionAllDecorator('hg.admin')
626 571 @CSRFRequired()
627 @view_config(
628 route_name='admin_settings_hooks_update', request_method='POST',
629 renderer='rhodecode:templates/admin/settings/settings.mako')
630 @view_config(
631 route_name='admin_settings_hooks_delete', request_method='POST',
632 renderer='rhodecode:templates/admin/settings/settings.mako')
633 572 def settings_hooks_update(self):
634 573 _ = self.request.translate
635 574 c = self.load_default_context()
636 575 c.active = 'hooks'
637 576 if c.visual.allow_custom_hooks_settings:
638 577 ui_key = self.request.POST.get('new_hook_ui_key')
639 578 ui_value = self.request.POST.get('new_hook_ui_value')
640 579
641 580 hook_id = self.request.POST.get('hook_id')
642 581 new_hook = False
643 582
644 583 model = SettingsModel()
645 584 try:
646 585 if ui_value and ui_key:
647 586 model.create_or_update_hook(ui_key, ui_value)
648 587 h.flash(_('Added new hook'), category='success')
649 588 new_hook = True
650 589 elif hook_id:
651 590 RhodeCodeUi.delete(hook_id)
652 591 Session().commit()
653 592
654 593 # check for edits
655 594 update = False
656 595 _d = self.request.POST.dict_of_lists()
657 596 for k, v in zip(_d.get('hook_ui_key', []),
658 597 _d.get('hook_ui_value_new', [])):
659 598 model.create_or_update_hook(k, v)
660 599 update = True
661 600
662 601 if update and not new_hook:
663 602 h.flash(_('Updated hooks'), category='success')
664 603 Session().commit()
665 604 except Exception:
666 605 log.exception("Exception during hook creation")
667 606 h.flash(_('Error occurred during hook creation'),
668 607 category='error')
669 608
670 609 raise HTTPFound(h.route_path('admin_settings_hooks'))
671 610
672 611 @LoginRequired()
673 612 @HasPermissionAllDecorator('hg.admin')
674 @view_config(
675 route_name='admin_settings_search', request_method='GET',
676 renderer='rhodecode:templates/admin/settings/settings.mako')
677 613 def settings_search(self):
678 614 c = self.load_default_context()
679 615 c.active = 'search'
680 616
681 617 c.searcher = searcher_from_config(self.request.registry.settings)
682 618 c.statistics = c.searcher.statistics(self.request.translate)
683 619
684 620 return self._get_template_context(c)
685 621
686 622 @LoginRequired()
687 623 @HasPermissionAllDecorator('hg.admin')
688 @view_config(
689 route_name='admin_settings_automation', request_method='GET',
690 renderer='rhodecode:templates/admin/settings/settings.mako')
691 624 def settings_automation(self):
692 625 c = self.load_default_context()
693 626 c.active = 'automation'
694 627
695 628 return self._get_template_context(c)
696 629
697 630 @LoginRequired()
698 631 @HasPermissionAllDecorator('hg.admin')
699 @view_config(
700 route_name='admin_settings_labs', request_method='GET',
701 renderer='rhodecode:templates/admin/settings/settings.mako')
702 632 def settings_labs(self):
703 633 c = self.load_default_context()
704 634 if not c.labs_active:
705 635 raise HTTPFound(h.route_path('admin_settings'))
706 636
707 637 c.active = 'labs'
708 638 c.lab_settings = _LAB_SETTINGS
709 639
710 640 data = render('rhodecode:templates/admin/settings/settings.mako',
711 641 self._get_template_context(c), self.request)
712 642 html = formencode.htmlfill.render(
713 643 data,
714 644 defaults=self._form_defaults(),
715 645 encoding="UTF-8",
716 646 force_defaults=False
717 647 )
718 648 return Response(html)
719 649
720 650 @LoginRequired()
721 651 @HasPermissionAllDecorator('hg.admin')
722 652 @CSRFRequired()
723 @view_config(
724 route_name='admin_settings_labs_update', request_method='POST',
725 renderer='rhodecode:templates/admin/settings/settings.mako')
726 653 def settings_labs_update(self):
727 654 _ = self.request.translate
728 655 c = self.load_default_context()
729 656 c.active = 'labs'
730 657
731 658 application_form = LabsSettingsForm(self.request.translate)()
732 659 try:
733 660 form_result = application_form.to_python(dict(self.request.POST))
734 661 except formencode.Invalid as errors:
735 662 h.flash(
736 663 _("Some form inputs contain invalid data."),
737 664 category='error')
738 665 data = render('rhodecode:templates/admin/settings/settings.mako',
739 666 self._get_template_context(c), self.request)
740 667 html = formencode.htmlfill.render(
741 668 data,
742 669 defaults=errors.value,
743 670 errors=errors.error_dict or {},
744 671 prefix_error=False,
745 672 encoding="UTF-8",
746 673 force_defaults=False
747 674 )
748 675 return Response(html)
749 676
750 677 try:
751 678 session = Session()
752 679 for setting in _LAB_SETTINGS:
753 680 setting_name = setting.key[len('rhodecode_'):]
754 681 sett = SettingsModel().create_or_update_setting(
755 682 setting_name, form_result[setting.key], setting.type)
756 683 session.add(sett)
757 684
758 685 except Exception:
759 686 log.exception('Exception while updating lab settings')
760 687 h.flash(_('Error occurred during updating labs settings'),
761 688 category='error')
762 689 else:
763 690 Session().commit()
764 691 SettingsModel().invalidate_settings_cache()
765 692 h.flash(_('Updated Labs settings'), category='success')
766 693 raise HTTPFound(h.route_path('admin_settings_labs'))
767 694
768 695 data = render('rhodecode:templates/admin/settings/settings.mako',
769 696 self._get_template_context(c), self.request)
770 697 html = formencode.htmlfill.render(
771 698 data,
772 699 defaults=self._form_defaults(),
773 700 encoding="UTF-8",
774 701 force_defaults=False
775 702 )
776 703 return Response(html)
777 704
778 705
779 706 # :param key: name of the setting including the 'rhodecode_' prefix
780 707 # :param type: the RhodeCodeSetting type to use.
781 708 # :param group: the i18ned group in which we should dispaly this setting
782 709 # :param label: the i18ned label we should display for this setting
783 710 # :param help: the i18ned help we should dispaly for this setting
784 711 LabSetting = collections.namedtuple(
785 712 'LabSetting', ('key', 'type', 'group', 'label', 'help'))
786 713
787 714
788 715 # This list has to be kept in sync with the form
789 716 # rhodecode.model.forms.LabsSettingsForm.
790 717 _LAB_SETTINGS = [
791 718
792 719 ]
@@ -1,59 +1,56 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 from pyramid.view import view_config
23
24 24
25 25 from rhodecode.apps._base import BaseAppView
26 26 from rhodecode.apps.svn_support.utils import generate_mod_dav_svn_config
27 27 from rhodecode.lib.auth import (
28 28 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 class SvnConfigAdminSettingsView(BaseAppView):
33 class AdminSvnConfigView(BaseAppView):
34 34
35 35 @LoginRequired()
36 36 @HasPermissionAllDecorator('hg.admin')
37 37 @CSRFRequired()
38 @view_config(
39 route_name='admin_settings_vcs_svn_generate_cfg',
40 request_method='POST', renderer='json')
41 38 def vcs_svn_generate_config(self):
42 39 _ = self.request.translate
43 40 try:
44 41 file_path = generate_mod_dav_svn_config(self.request.registry)
45 42 msg = {
46 43 'message': _('Apache configuration for Subversion generated at `{}`.').format(file_path),
47 44 'level': 'success',
48 45 }
49 46 except Exception:
50 47 log.exception(
51 48 'Exception while generating the Apache '
52 49 'configuration for Subversion.')
53 50 msg = {
54 51 'message': _('Failed to generate the Apache configuration for Subversion.'),
55 52 'level': 'error',
56 53 }
57 54
58 55 data = {'message': msg}
59 56 return data
@@ -1,206 +1,200 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 urllib2
23 23
24 from pyramid.view import view_config
24
25 25
26 26 import rhodecode
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.apps._base.navigation import navigation_list
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
31 31 from rhodecode.lib.utils2 import str2bool
32 32 from rhodecode.lib import system_info
33 33 from rhodecode.model.update import UpdateModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class AdminSystemInfoSettingsView(BaseAppView):
39 39 def load_default_context(self):
40 40 c = self._get_local_tmpl_context()
41 41 return c
42 42
43 43 @LoginRequired()
44 44 @HasPermissionAllDecorator('hg.admin')
45 @view_config(
46 route_name='admin_settings_system', request_method='GET',
47 renderer='rhodecode:templates/admin/settings/settings.mako')
48 45 def settings_system_info(self):
49 46 _ = self.request.translate
50 47 c = self.load_default_context()
51 48
52 49 c.active = 'system'
53 50 c.navlist = navigation_list(self.request)
54 51
55 52 # TODO(marcink), figure out how to allow only selected users to do this
56 53 c.allowed_to_snapshot = self._rhodecode_user.admin
57 54
58 55 snapshot = str2bool(self.request.params.get('snapshot'))
59 56
60 57 c.rhodecode_update_url = UpdateModel().get_update_url()
61 58 server_info = system_info.get_system_info(self.request.environ)
62 59
63 60 for key, val in server_info.items():
64 61 setattr(c, key, val)
65 62
66 63 def val(name, subkey='human_value'):
67 64 return server_info[name][subkey]
68 65
69 66 def state(name):
70 67 return server_info[name]['state']
71 68
72 69 def val2(name):
73 70 val = server_info[name]['human_value']
74 71 state = server_info[name]['state']
75 72 return val, state
76 73
77 74 update_info_msg = _('Note: please make sure this server can '
78 75 'access `${url}` for the update link to work',
79 76 mapping=dict(url=c.rhodecode_update_url))
80 77 version = UpdateModel().get_stored_version()
81 78 is_outdated = UpdateModel().is_outdated(
82 79 rhodecode.__version__, version)
83 80 update_state = {
84 81 'type': 'warning',
85 82 'message': 'New version available: {}'.format(version)
86 83 } \
87 84 if is_outdated else {}
88 85 c.data_items = [
89 86 # update info
90 87 (_('Update info'), h.literal(
91 88 '<span class="link" id="check_for_update" >%s.</span>' % (
92 89 _('Check for updates')) +
93 90 '<br/> <span >%s.</span>' % (update_info_msg)
94 91 ), ''),
95 92
96 93 # RhodeCode specific
97 94 (_('RhodeCode Version'), val('rhodecode_app')['text'], state('rhodecode_app')),
98 95 (_('Latest version'), version, update_state),
99 96 (_('RhodeCode Base URL'), val('rhodecode_config')['config'].get('app.base_url'), state('rhodecode_config')),
100 97 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
101 98 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
102 99 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
103 100 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
104 101 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
105 102 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
106 103 ('', '', ''), # spacer
107 104
108 105 # Database
109 106 (_('Database'), val('database')['url'], state('database')),
110 107 (_('Database version'), val('database')['version'], state('database')),
111 108 ('', '', ''), # spacer
112 109
113 110 # Platform/Python
114 111 (_('Platform'), val('platform')['name'], state('platform')),
115 112 (_('Platform UUID'), val('platform')['uuid'], state('platform')),
116 113 (_('Lang'), val('locale'), state('locale')),
117 114 (_('Python version'), val('python')['version'], state('python')),
118 115 (_('Python path'), val('python')['executable'], state('python')),
119 116 ('', '', ''), # spacer
120 117
121 118 # Systems stats
122 119 (_('CPU'), val('cpu')['text'], state('cpu')),
123 120 (_('Load'), val('load')['text'], state('load')),
124 121 (_('Memory'), val('memory')['text'], state('memory')),
125 122 (_('Uptime'), val('uptime')['text'], state('uptime')),
126 123 ('', '', ''), # spacer
127 124
128 125 # ulimit
129 126 (_('Ulimit'), val('ulimit')['text'], state('ulimit')),
130 127
131 128 # Repo storage
132 129 (_('Storage location'), val('storage')['path'], state('storage')),
133 130 (_('Storage info'), val('storage')['text'], state('storage')),
134 131 (_('Storage inodes'), val('storage_inodes')['text'], state('storage_inodes')),
135 132
136 133 (_('Gist storage location'), val('storage_gist')['path'], state('storage_gist')),
137 134 (_('Gist storage info'), val('storage_gist')['text'], state('storage_gist')),
138 135
139 136 (_('Archive cache storage location'), val('storage_archive')['path'], state('storage_archive')),
140 137 (_('Archive cache info'), val('storage_archive')['text'], state('storage_archive')),
141 138
142 139 (_('Temp storage location'), val('storage_temp')['path'], state('storage_temp')),
143 140 (_('Temp storage info'), val('storage_temp')['text'], state('storage_temp')),
144 141
145 142 (_('Search info'), val('search')['text'], state('search')),
146 143 (_('Search location'), val('search')['location'], state('search')),
147 144 ('', '', ''), # spacer
148 145
149 146 # VCS specific
150 147 (_('VCS Backends'), val('vcs_backends'), state('vcs_backends')),
151 148 (_('VCS Server'), val('vcs_server')['text'], state('vcs_server')),
152 149 (_('GIT'), val('git'), state('git')),
153 150 (_('HG'), val('hg'), state('hg')),
154 151 (_('SVN'), val('svn'), state('svn')),
155 152
156 153 ]
157 154
158 155 c.vcsserver_data_items = [
159 156 (k, v) for k,v in (val('vcs_server_config') or {}).items()
160 157 ]
161 158
162 159 if snapshot:
163 160 if c.allowed_to_snapshot:
164 161 c.data_items.pop(0) # remove server info
165 162 self.request.override_renderer = 'admin/settings/settings_system_snapshot.mako'
166 163 else:
167 164 h.flash('You are not allowed to do this', category='warning')
168 165 return self._get_template_context(c)
169 166
170 167 @LoginRequired()
171 168 @HasPermissionAllDecorator('hg.admin')
172 @view_config(
173 route_name='admin_settings_system_update', request_method='GET',
174 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
175 169 def settings_system_info_check_update(self):
176 170 _ = self.request.translate
177 171 c = self.load_default_context()
178 172
179 173 update_url = UpdateModel().get_update_url()
180 174
181 175 _err = lambda s: '<div style="color:#ff8888; padding:4px 0px">{}</div>'.format(s)
182 176 try:
183 177 data = UpdateModel().get_update_data(update_url)
184 178 except urllib2.URLError as e:
185 179 log.exception("Exception contacting upgrade server")
186 180 self.request.override_renderer = 'string'
187 181 return _err('Failed to contact upgrade server: %r' % e)
188 182 except ValueError as e:
189 183 log.exception("Bad data sent from update server")
190 184 self.request.override_renderer = 'string'
191 185 return _err('Bad data sent from update server')
192 186
193 187 latest = data['versions'][0]
194 188
195 189 c.update_url = update_url
196 190 c.latest_data = latest
197 191 c.latest_ver = latest['version']
198 192 c.cur_ver = rhodecode.__version__
199 193 c.should_upgrade = False
200 194
201 195 is_oudated = UpdateModel().is_outdated(c.cur_ver, c.latest_ver)
202 196 if is_oudated:
203 197 c.should_upgrade = True
204 198 c.important_notices = latest['general']
205 199 UpdateModel().store_version(latest['version'])
206 200 return self._get_template_context(c)
@@ -1,268 +1,254 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
27
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
35 35 from rhodecode.lib import helpers as h, audit_logger
36 36 from rhodecode.lib.utils2 import safe_unicode
37 37
38 38 from rhodecode.model.forms import UserGroupForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.scm import UserGroupList
41 41 from rhodecode.model.db import (
42 42 or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.model.user_group import UserGroupModel
45 45 from rhodecode.model.db import true
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class AdminUserGroupsView(BaseAppView, DataGridAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54
55 54 PermissionModel().set_global_permission_choices(
56 55 c, gettext_translator=self.request.translate)
57
58 56 return c
59 57
60 58 # permission check in data loading of
61 59 # `user_groups_list_data` via UserGroupList
62 60 @LoginRequired()
63 61 @NotAnonymous()
64 @view_config(
65 route_name='user_groups', request_method='GET',
66 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
67 62 def user_groups_list(self):
68 63 c = self.load_default_context()
69 64 return self._get_template_context(c)
70 65
71 66 # permission check inside
72 67 @LoginRequired()
73 68 @NotAnonymous()
74 @view_config(
75 route_name='user_groups_data', request_method='GET',
76 renderer='json_ext', xhr=True)
77 69 def user_groups_list_data(self):
78 70 self.load_default_context()
79 71 column_map = {
80 72 'active': 'users_group_active',
81 73 'description': 'user_group_description',
82 74 'members': 'members_total',
83 75 'owner': 'user_username',
84 76 'sync': 'group_data'
85 77 }
86 78 draw, start, limit = self._extract_chunk(self.request)
87 79 search_q, order_by, order_dir = self._extract_ordering(
88 80 self.request, column_map=column_map)
89 81
90 82 _render = self.request.get_partial_renderer(
91 83 'rhodecode:templates/data_table/_dt_elements.mako')
92 84
93 85 def user_group_name(user_group_name):
94 86 return _render("user_group_name", user_group_name)
95 87
96 88 def user_group_actions(user_group_id, user_group_name):
97 89 return _render("user_group_actions", user_group_id, user_group_name)
98 90
99 91 def user_profile(username):
100 92 return _render('user_profile', username)
101 93
102 94 _perms = ['usergroup.admin']
103 95 allowed_ids = [-1] + self._rhodecode_user.user_group_acl_ids_from_stack(_perms)
104 96
105 97 user_groups_data_total_count = UserGroup.query()\
106 98 .filter(or_(
107 99 # generate multiple IN to fix limitation problems
108 100 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
109 101 ))\
110 102 .count()
111 103
112 104 user_groups_data_total_inactive_count = UserGroup.query()\
113 105 .filter(or_(
114 106 # generate multiple IN to fix limitation problems
115 107 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
116 108 ))\
117 109 .filter(UserGroup.users_group_active != true()).count()
118 110
119 111 member_count = count(UserGroupMember.user_id)
120 112 base_q = Session.query(
121 113 UserGroup.users_group_name,
122 114 UserGroup.user_group_description,
123 115 UserGroup.users_group_active,
124 116 UserGroup.users_group_id,
125 117 UserGroup.group_data,
126 118 User,
127 119 member_count.label('member_count')
128 120 ) \
129 121 .filter(or_(
130 122 # generate multiple IN to fix limitation problems
131 123 *in_filter_generator(UserGroup.users_group_id, allowed_ids)
132 124 )) \
133 125 .outerjoin(UserGroupMember, UserGroupMember.users_group_id == UserGroup.users_group_id) \
134 126 .join(User, User.user_id == UserGroup.user_id) \
135 127 .group_by(UserGroup, User)
136 128
137 129 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
138 130
139 131 if search_q:
140 132 like_expression = u'%{}%'.format(safe_unicode(search_q))
141 133 base_q = base_q.filter(or_(
142 134 UserGroup.users_group_name.ilike(like_expression),
143 135 ))
144 136 base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
145 137
146 138 user_groups_data_total_filtered_count = base_q.count()
147 139 user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
148 140
149 141 sort_defined = False
150 142 if order_by == 'members_total':
151 143 sort_col = member_count
152 144 sort_defined = True
153 145 elif order_by == 'user_username':
154 146 sort_col = User.username
155 147 else:
156 148 sort_col = getattr(UserGroup, order_by, None)
157 149
158 150 if sort_defined or sort_col:
159 151 if order_dir == 'asc':
160 152 sort_col = sort_col.asc()
161 153 else:
162 154 sort_col = sort_col.desc()
163 155
164 156 base_q = base_q.order_by(sort_col)
165 157 base_q = base_q.offset(start).limit(limit)
166 158
167 159 # authenticated access to user groups
168 160 auth_user_group_list = base_q.all()
169 161
170 162 user_groups_data = []
171 163 for user_gr in auth_user_group_list:
172 164 row = {
173 165 "users_group_name": user_group_name(user_gr.users_group_name),
174 166 "description": h.escape(user_gr.user_group_description),
175 167 "members": user_gr.member_count,
176 168 # NOTE(marcink): because of advanced query we
177 169 # need to load it like that
178 170 "sync": UserGroup._load_sync(
179 171 UserGroup._load_group_data(user_gr.group_data)),
180 172 "active": h.bool2icon(user_gr.users_group_active),
181 173 "owner": user_profile(user_gr.User.username),
182 174 "action": user_group_actions(
183 175 user_gr.users_group_id, user_gr.users_group_name)
184 176 }
185 177 user_groups_data.append(row)
186 178
187 179 data = ({
188 180 'draw': draw,
189 181 'data': user_groups_data,
190 182 'recordsTotal': user_groups_data_total_count,
191 183 'recordsTotalInactive': user_groups_data_total_inactive_count,
192 184 'recordsFiltered': user_groups_data_total_filtered_count,
193 185 'recordsFilteredInactive': user_groups_data_total_filtered_inactive_count,
194 186 })
195 187
196 188 return data
197 189
198 190 @LoginRequired()
199 191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
200 @view_config(
201 route_name='user_groups_new', request_method='GET',
202 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
203 192 def user_groups_new(self):
204 193 c = self.load_default_context()
205 194 return self._get_template_context(c)
206 195
207 196 @LoginRequired()
208 197 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
209 198 @CSRFRequired()
210 @view_config(
211 route_name='user_groups_create', request_method='POST',
212 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
213 199 def user_groups_create(self):
214 200 _ = self.request.translate
215 201 c = self.load_default_context()
216 202 users_group_form = UserGroupForm(self.request.translate)()
217 203
218 204 user_group_name = self.request.POST.get('users_group_name')
219 205 try:
220 206 form_result = users_group_form.to_python(dict(self.request.POST))
221 207 user_group = UserGroupModel().create(
222 208 name=form_result['users_group_name'],
223 209 description=form_result['user_group_description'],
224 210 owner=self._rhodecode_user.user_id,
225 211 active=form_result['users_group_active'])
226 212 Session().flush()
227 213 creation_data = user_group.get_api_data()
228 214 user_group_name = form_result['users_group_name']
229 215
230 216 audit_logger.store_web(
231 217 'user_group.create', action_data={'data': creation_data},
232 218 user=self._rhodecode_user)
233 219
234 220 user_group_link = h.link_to(
235 221 h.escape(user_group_name),
236 222 h.route_path(
237 223 'edit_user_group', user_group_id=user_group.users_group_id))
238 224 h.flash(h.literal(_('Created user group %(user_group_link)s')
239 225 % {'user_group_link': user_group_link}),
240 226 category='success')
241 227 Session().commit()
242 228 user_group_id = user_group.users_group_id
243 229 except formencode.Invalid as errors:
244 230
245 231 data = render(
246 232 'rhodecode:templates/admin/user_groups/user_group_add.mako',
247 233 self._get_template_context(c), self.request)
248 234 html = formencode.htmlfill.render(
249 235 data,
250 236 defaults=errors.value,
251 237 errors=errors.error_dict or {},
252 238 prefix_error=False,
253 239 encoding="UTF-8",
254 240 force_defaults=False
255 241 )
256 242 return Response(html)
257 243
258 244 except Exception:
259 245 log.exception("Exception creating user group")
260 246 h.flash(_('Error occurred during creation of user group %s') \
261 247 % user_group_name, category='error')
262 248 raise HTTPFound(h.route_path('user_groups_new'))
263 249
264 250 affected_user_ids = [self._rhodecode_user.user_id]
265 251 PermissionModel().trigger_permission_flush(affected_user_ids)
266 252
267 253 raise HTTPFound(
268 254 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1418 +1,1318 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.view import view_config
28 27 from pyramid.renderers import render
29 28 from pyramid.response import Response
30 29
31 30 from rhodecode import events
32 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
34 33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 34 from rhodecode.authentication.plugins import auth_rhodecode
36 35 from rhodecode.events import trigger
37 36 from rhodecode.model.db import true, UserNotice
38 37
39 38 from rhodecode.lib import audit_logger, rc_cache, auth
40 39 from rhodecode.lib.exceptions import (
41 40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
43 42 UserOwnsArtifactsException, DefaultUserException)
44 43 from rhodecode.lib.ext_json import json
45 44 from rhodecode.lib.auth import (
46 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
47 46 from rhodecode.lib import helpers as h
48 47 from rhodecode.lib.helpers import SqlPage
49 48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
50 49 from rhodecode.model.auth_token import AuthTokenModel
51 50 from rhodecode.model.forms import (
52 51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
53 52 UserExtraEmailForm, UserExtraIpForm)
54 53 from rhodecode.model.permission import PermissionModel
55 54 from rhodecode.model.repo_group import RepoGroupModel
56 55 from rhodecode.model.ssh_key import SshKeyModel
57 56 from rhodecode.model.user import UserModel
58 57 from rhodecode.model.user_group import UserGroupModel
59 58 from rhodecode.model.db import (
60 59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
61 60 UserApiKeys, UserSshKeys, RepoGroup)
62 61 from rhodecode.model.meta import Session
63 62
64 63 log = logging.getLogger(__name__)
65 64
66 65
67 66 class AdminUsersView(BaseAppView, DataGridAppView):
68 67
69 68 def load_default_context(self):
70 69 c = self._get_local_tmpl_context()
71 70 return c
72 71
73 72 @LoginRequired()
74 73 @HasPermissionAllDecorator('hg.admin')
75 @view_config(
76 route_name='users', request_method='GET',
77 renderer='rhodecode:templates/admin/users/users.mako')
78 74 def users_list(self):
79 75 c = self.load_default_context()
80 76 return self._get_template_context(c)
81 77
82 78 @LoginRequired()
83 79 @HasPermissionAllDecorator('hg.admin')
84 @view_config(
85 # renderer defined below
86 route_name='users_data', request_method='GET',
87 renderer='json_ext', xhr=True)
88 80 def users_list_data(self):
89 81 self.load_default_context()
90 82 column_map = {
91 83 'first_name': 'name',
92 84 'last_name': 'lastname',
93 85 }
94 86 draw, start, limit = self._extract_chunk(self.request)
95 87 search_q, order_by, order_dir = self._extract_ordering(
96 88 self.request, column_map=column_map)
97 89 _render = self.request.get_partial_renderer(
98 90 'rhodecode:templates/data_table/_dt_elements.mako')
99 91
100 92 def user_actions(user_id, username):
101 93 return _render("user_actions", user_id, username)
102 94
103 95 users_data_total_count = User.query()\
104 96 .filter(User.username != User.DEFAULT_USER) \
105 97 .count()
106 98
107 99 users_data_total_inactive_count = User.query()\
108 100 .filter(User.username != User.DEFAULT_USER) \
109 101 .filter(User.active != true())\
110 102 .count()
111 103
112 104 # json generate
113 105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
114 106 base_inactive_q = base_q.filter(User.active != true())
115 107
116 108 if search_q:
117 109 like_expression = u'%{}%'.format(safe_unicode(search_q))
118 110 base_q = base_q.filter(or_(
119 111 User.username.ilike(like_expression),
120 112 User._email.ilike(like_expression),
121 113 User.name.ilike(like_expression),
122 114 User.lastname.ilike(like_expression),
123 115 ))
124 116 base_inactive_q = base_q.filter(User.active != true())
125 117
126 118 users_data_total_filtered_count = base_q.count()
127 119 users_data_total_filtered_inactive_count = base_inactive_q.count()
128 120
129 121 sort_col = getattr(User, order_by, None)
130 122 if sort_col:
131 123 if order_dir == 'asc':
132 124 # handle null values properly to order by NULL last
133 125 if order_by in ['last_activity']:
134 126 sort_col = coalesce(sort_col, datetime.date.max)
135 127 sort_col = sort_col.asc()
136 128 else:
137 129 # handle null values properly to order by NULL last
138 130 if order_by in ['last_activity']:
139 131 sort_col = coalesce(sort_col, datetime.date.min)
140 132 sort_col = sort_col.desc()
141 133
142 134 base_q = base_q.order_by(sort_col)
143 135 base_q = base_q.offset(start).limit(limit)
144 136
145 137 users_list = base_q.all()
146 138
147 139 users_data = []
148 140 for user in users_list:
149 141 users_data.append({
150 142 "username": h.gravatar_with_user(self.request, user.username),
151 143 "email": user.email,
152 144 "first_name": user.first_name,
153 145 "last_name": user.last_name,
154 146 "last_login": h.format_date(user.last_login),
155 147 "last_activity": h.format_date(user.last_activity),
156 148 "active": h.bool2icon(user.active),
157 149 "active_raw": user.active,
158 150 "admin": h.bool2icon(user.admin),
159 151 "extern_type": user.extern_type,
160 152 "extern_name": user.extern_name,
161 153 "action": user_actions(user.user_id, user.username),
162 154 })
163 155 data = ({
164 156 'draw': draw,
165 157 'data': users_data,
166 158 'recordsTotal': users_data_total_count,
167 159 'recordsFiltered': users_data_total_filtered_count,
168 160 'recordsTotalInactive': users_data_total_inactive_count,
169 161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
170 162 })
171 163
172 164 return data
173 165
174 166 def _set_personal_repo_group_template_vars(self, c_obj):
175 167 DummyUser = AttributeDict({
176 168 'username': '${username}',
177 169 'user_id': '${user_id}',
178 170 })
179 171 c_obj.default_create_repo_group = RepoGroupModel() \
180 172 .get_default_create_personal_repo_group()
181 173 c_obj.personal_repo_group_name = RepoGroupModel() \
182 174 .get_personal_group_name(DummyUser)
183 175
184 176 @LoginRequired()
185 177 @HasPermissionAllDecorator('hg.admin')
186 @view_config(
187 route_name='users_new', request_method='GET',
188 renderer='rhodecode:templates/admin/users/user_add.mako')
189 178 def users_new(self):
190 179 _ = self.request.translate
191 180 c = self.load_default_context()
192 181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
193 182 self._set_personal_repo_group_template_vars(c)
194 183 return self._get_template_context(c)
195 184
196 185 @LoginRequired()
197 186 @HasPermissionAllDecorator('hg.admin')
198 187 @CSRFRequired()
199 @view_config(
200 route_name='users_create', request_method='POST',
201 renderer='rhodecode:templates/admin/users/user_add.mako')
202 188 def users_create(self):
203 189 _ = self.request.translate
204 190 c = self.load_default_context()
205 191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
206 192 user_model = UserModel()
207 193 user_form = UserForm(self.request.translate)()
208 194 try:
209 195 form_result = user_form.to_python(dict(self.request.POST))
210 196 user = user_model.create(form_result)
211 197 Session().flush()
212 198 creation_data = user.get_api_data()
213 199 username = form_result['username']
214 200
215 201 audit_logger.store_web(
216 202 'user.create', action_data={'data': creation_data},
217 203 user=c.rhodecode_user)
218 204
219 205 user_link = h.link_to(
220 206 h.escape(username),
221 207 h.route_path('user_edit', user_id=user.user_id))
222 208 h.flash(h.literal(_('Created user %(user_link)s')
223 209 % {'user_link': user_link}), category='success')
224 210 Session().commit()
225 211 except formencode.Invalid as errors:
226 212 self._set_personal_repo_group_template_vars(c)
227 213 data = render(
228 214 'rhodecode:templates/admin/users/user_add.mako',
229 215 self._get_template_context(c), self.request)
230 216 html = formencode.htmlfill.render(
231 217 data,
232 218 defaults=errors.value,
233 219 errors=errors.error_dict or {},
234 220 prefix_error=False,
235 221 encoding="UTF-8",
236 222 force_defaults=False
237 223 )
238 224 return Response(html)
239 225 except UserCreationError as e:
240 226 h.flash(e, 'error')
241 227 except Exception:
242 228 log.exception("Exception creation of user")
243 229 h.flash(_('Error occurred during creation of user %s')
244 230 % self.request.POST.get('username'), category='error')
245 231 raise HTTPFound(h.route_path('users'))
246 232
247 233
248 234 class UsersView(UserAppView):
249 235 ALLOW_SCOPED_TOKENS = False
250 236 """
251 237 This view has alternative version inside EE, if modified please take a look
252 238 in there as well.
253 239 """
254 240
255 241 def get_auth_plugins(self):
256 242 valid_plugins = []
257 243 authn_registry = get_authn_registry(self.request.registry)
258 244 for plugin in authn_registry.get_plugins_for_authentication():
259 245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
260 246 valid_plugins.append(plugin)
261 247 elif plugin.name == 'rhodecode':
262 248 valid_plugins.append(plugin)
263 249
264 250 # extend our choices if user has set a bound plugin which isn't enabled at the
265 251 # moment
266 252 extern_type = self.db_user.extern_type
267 253 if extern_type not in [x.uid for x in valid_plugins]:
268 254 try:
269 255 plugin = authn_registry.get_plugin_by_uid(extern_type)
270 256 if plugin:
271 257 valid_plugins.append(plugin)
272 258
273 259 except Exception:
274 260 log.exception(
275 261 'Could not extend user plugins with `{}`'.format(extern_type))
276 262 return valid_plugins
277 263
278 264 def load_default_context(self):
279 265 req = self.request
280 266
281 267 c = self._get_local_tmpl_context()
282 268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
283 269 c.allowed_languages = [
284 270 ('en', 'English (en)'),
285 271 ('de', 'German (de)'),
286 272 ('fr', 'French (fr)'),
287 273 ('it', 'Italian (it)'),
288 274 ('ja', 'Japanese (ja)'),
289 275 ('pl', 'Polish (pl)'),
290 276 ('pt', 'Portuguese (pt)'),
291 277 ('ru', 'Russian (ru)'),
292 278 ('zh', 'Chinese (zh)'),
293 279 ]
294 280
295 281 c.allowed_extern_types = [
296 282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
297 283 ]
298 284 perms = req.registry.settings.get('available_permissions')
299 285 if not perms:
300 286 # inject info about available permissions
301 287 auth.set_available_permissions(req.registry.settings)
302 288
303 289 c.available_permissions = req.registry.settings['available_permissions']
304 290 PermissionModel().set_global_permission_choices(
305 291 c, gettext_translator=req.translate)
306 292
307 293 return c
308 294
309 295 @LoginRequired()
310 296 @HasPermissionAllDecorator('hg.admin')
311 297 @CSRFRequired()
312 @view_config(
313 route_name='user_update', request_method='POST',
314 renderer='rhodecode:templates/admin/users/user_edit.mako')
315 298 def user_update(self):
316 299 _ = self.request.translate
317 300 c = self.load_default_context()
318 301
319 302 user_id = self.db_user_id
320 303 c.user = self.db_user
321 304
322 305 c.active = 'profile'
323 306 c.extern_type = c.user.extern_type
324 307 c.extern_name = c.user.extern_name
325 308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
326 309 available_languages = [x[0] for x in c.allowed_languages]
327 310 _form = UserForm(self.request.translate, edit=True,
328 311 available_languages=available_languages,
329 312 old_data={'user_id': user_id,
330 313 'email': c.user.email})()
331 314 form_result = {}
332 315 old_values = c.user.get_api_data()
333 316 try:
334 317 form_result = _form.to_python(dict(self.request.POST))
335 318 skip_attrs = ['extern_name']
336 319 # TODO: plugin should define if username can be updated
337 320 if c.extern_type != "rhodecode":
338 321 # forbid updating username for external accounts
339 322 skip_attrs.append('username')
340 323
341 324 UserModel().update_user(
342 325 user_id, skip_attrs=skip_attrs, **form_result)
343 326
344 327 audit_logger.store_web(
345 328 'user.edit', action_data={'old_data': old_values},
346 329 user=c.rhodecode_user)
347 330
348 331 Session().commit()
349 332 h.flash(_('User updated successfully'), category='success')
350 333 except formencode.Invalid as errors:
351 334 data = render(
352 335 'rhodecode:templates/admin/users/user_edit.mako',
353 336 self._get_template_context(c), self.request)
354 337 html = formencode.htmlfill.render(
355 338 data,
356 339 defaults=errors.value,
357 340 errors=errors.error_dict or {},
358 341 prefix_error=False,
359 342 encoding="UTF-8",
360 343 force_defaults=False
361 344 )
362 345 return Response(html)
363 346 except UserCreationError as e:
364 347 h.flash(e, 'error')
365 348 except Exception:
366 349 log.exception("Exception updating user")
367 350 h.flash(_('Error occurred during update of user %s')
368 351 % form_result.get('username'), category='error')
369 352 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
370 353
371 354 @LoginRequired()
372 355 @HasPermissionAllDecorator('hg.admin')
373 356 @CSRFRequired()
374 @view_config(
375 route_name='user_delete', request_method='POST',
376 renderer='rhodecode:templates/admin/users/user_edit.mako')
377 357 def user_delete(self):
378 358 _ = self.request.translate
379 359 c = self.load_default_context()
380 360 c.user = self.db_user
381 361
382 362 _repos = c.user.repositories
383 363 _repo_groups = c.user.repository_groups
384 364 _user_groups = c.user.user_groups
385 365 _pull_requests = c.user.user_pull_requests
386 366 _artifacts = c.user.artifacts
387 367
388 368 handle_repos = None
389 369 handle_repo_groups = None
390 370 handle_user_groups = None
391 371 handle_pull_requests = None
392 372 handle_artifacts = None
393 373
394 374 # calls for flash of handle based on handle case detach or delete
395 375 def set_handle_flash_repos():
396 376 handle = handle_repos
397 377 if handle == 'detach':
398 378 h.flash(_('Detached %s repositories') % len(_repos),
399 379 category='success')
400 380 elif handle == 'delete':
401 381 h.flash(_('Deleted %s repositories') % len(_repos),
402 382 category='success')
403 383
404 384 def set_handle_flash_repo_groups():
405 385 handle = handle_repo_groups
406 386 if handle == 'detach':
407 387 h.flash(_('Detached %s repository groups') % len(_repo_groups),
408 388 category='success')
409 389 elif handle == 'delete':
410 390 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
411 391 category='success')
412 392
413 393 def set_handle_flash_user_groups():
414 394 handle = handle_user_groups
415 395 if handle == 'detach':
416 396 h.flash(_('Detached %s user groups') % len(_user_groups),
417 397 category='success')
418 398 elif handle == 'delete':
419 399 h.flash(_('Deleted %s user groups') % len(_user_groups),
420 400 category='success')
421 401
422 402 def set_handle_flash_pull_requests():
423 403 handle = handle_pull_requests
424 404 if handle == 'detach':
425 405 h.flash(_('Detached %s pull requests') % len(_pull_requests),
426 406 category='success')
427 407 elif handle == 'delete':
428 408 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
429 409 category='success')
430 410
431 411 def set_handle_flash_artifacts():
432 412 handle = handle_artifacts
433 413 if handle == 'detach':
434 414 h.flash(_('Detached %s artifacts') % len(_artifacts),
435 415 category='success')
436 416 elif handle == 'delete':
437 417 h.flash(_('Deleted %s artifacts') % len(_artifacts),
438 418 category='success')
439 419
440 420 handle_user = User.get_first_super_admin()
441 421 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
442 422 if handle_user_id:
443 423 # NOTE(marcink): we get new owner for objects...
444 424 handle_user = User.get_or_404(handle_user_id)
445 425
446 426 if _repos and self.request.POST.get('user_repos'):
447 427 handle_repos = self.request.POST['user_repos']
448 428
449 429 if _repo_groups and self.request.POST.get('user_repo_groups'):
450 430 handle_repo_groups = self.request.POST['user_repo_groups']
451 431
452 432 if _user_groups and self.request.POST.get('user_user_groups'):
453 433 handle_user_groups = self.request.POST['user_user_groups']
454 434
455 435 if _pull_requests and self.request.POST.get('user_pull_requests'):
456 436 handle_pull_requests = self.request.POST['user_pull_requests']
457 437
458 438 if _artifacts and self.request.POST.get('user_artifacts'):
459 439 handle_artifacts = self.request.POST['user_artifacts']
460 440
461 441 old_values = c.user.get_api_data()
462 442
463 443 try:
464 444
465 445 UserModel().delete(
466 446 c.user,
467 447 handle_repos=handle_repos,
468 448 handle_repo_groups=handle_repo_groups,
469 449 handle_user_groups=handle_user_groups,
470 450 handle_pull_requests=handle_pull_requests,
471 451 handle_artifacts=handle_artifacts,
472 452 handle_new_owner=handle_user
473 453 )
474 454
475 455 audit_logger.store_web(
476 456 'user.delete', action_data={'old_data': old_values},
477 457 user=c.rhodecode_user)
478 458
479 459 Session().commit()
480 460 set_handle_flash_repos()
481 461 set_handle_flash_repo_groups()
482 462 set_handle_flash_user_groups()
483 463 set_handle_flash_pull_requests()
484 464 set_handle_flash_artifacts()
485 465 username = h.escape(old_values['username'])
486 466 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
487 467 except (UserOwnsReposException, UserOwnsRepoGroupsException,
488 468 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
489 469 UserOwnsArtifactsException, DefaultUserException) as e:
490 470 h.flash(e, category='warning')
491 471 except Exception:
492 472 log.exception("Exception during deletion of user")
493 473 h.flash(_('An error occurred during deletion of user'),
494 474 category='error')
495 475 raise HTTPFound(h.route_path('users'))
496 476
497 477 @LoginRequired()
498 478 @HasPermissionAllDecorator('hg.admin')
499 @view_config(
500 route_name='user_edit', request_method='GET',
501 renderer='rhodecode:templates/admin/users/user_edit.mako')
502 479 def user_edit(self):
503 480 _ = self.request.translate
504 481 c = self.load_default_context()
505 482 c.user = self.db_user
506 483
507 484 c.active = 'profile'
508 485 c.extern_type = c.user.extern_type
509 486 c.extern_name = c.user.extern_name
510 487 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
511 488
512 489 defaults = c.user.get_dict()
513 490 defaults.update({'language': c.user.user_data.get('language')})
514 491
515 492 data = render(
516 493 'rhodecode:templates/admin/users/user_edit.mako',
517 494 self._get_template_context(c), self.request)
518 495 html = formencode.htmlfill.render(
519 496 data,
520 497 defaults=defaults,
521 498 encoding="UTF-8",
522 499 force_defaults=False
523 500 )
524 501 return Response(html)
525 502
526 503 @LoginRequired()
527 504 @HasPermissionAllDecorator('hg.admin')
528 @view_config(
529 route_name='user_edit_advanced', request_method='GET',
530 renderer='rhodecode:templates/admin/users/user_edit.mako')
531 505 def user_edit_advanced(self):
532 506 _ = self.request.translate
533 507 c = self.load_default_context()
534 508
535 509 user_id = self.db_user_id
536 510 c.user = self.db_user
537 511
538 512 c.detach_user = User.get_first_super_admin()
539 513 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
540 514 if detach_user_id:
541 515 c.detach_user = User.get_or_404(detach_user_id)
542 516
543 517 c.active = 'advanced'
544 518 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
545 519 c.personal_repo_group_name = RepoGroupModel()\
546 520 .get_personal_group_name(c.user)
547 521
548 522 c.user_to_review_rules = sorted(
549 523 (x.user for x in c.user.user_review_rules),
550 524 key=lambda u: u.username.lower())
551 525
552 526 defaults = c.user.get_dict()
553 527
554 528 # Interim workaround if the user participated on any pull requests as a
555 529 # reviewer.
556 530 has_review = len(c.user.reviewer_pull_requests)
557 531 c.can_delete_user = not has_review
558 532 c.can_delete_user_message = ''
559 533 inactive_link = h.link_to(
560 534 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
561 535 if has_review == 1:
562 536 c.can_delete_user_message = h.literal(_(
563 537 'The user participates as reviewer in {} pull request and '
564 538 'cannot be deleted. \nYou can set the user to '
565 539 '"{}" instead of deleting it.').format(
566 540 has_review, inactive_link))
567 541 elif has_review:
568 542 c.can_delete_user_message = h.literal(_(
569 543 'The user participates as reviewer in {} pull requests and '
570 544 'cannot be deleted. \nYou can set the user to '
571 545 '"{}" instead of deleting it.').format(
572 546 has_review, inactive_link))
573 547
574 548 data = render(
575 549 'rhodecode:templates/admin/users/user_edit.mako',
576 550 self._get_template_context(c), self.request)
577 551 html = formencode.htmlfill.render(
578 552 data,
579 553 defaults=defaults,
580 554 encoding="UTF-8",
581 555 force_defaults=False
582 556 )
583 557 return Response(html)
584 558
585 559 @LoginRequired()
586 560 @HasPermissionAllDecorator('hg.admin')
587 @view_config(
588 route_name='user_edit_global_perms', request_method='GET',
589 renderer='rhodecode:templates/admin/users/user_edit.mako')
590 561 def user_edit_global_perms(self):
591 562 _ = self.request.translate
592 563 c = self.load_default_context()
593 564 c.user = self.db_user
594 565
595 566 c.active = 'global_perms'
596 567
597 568 c.default_user = User.get_default_user()
598 569 defaults = c.user.get_dict()
599 570 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
600 571 defaults.update(c.default_user.get_default_perms())
601 572 defaults.update(c.user.get_default_perms())
602 573
603 574 data = render(
604 575 'rhodecode:templates/admin/users/user_edit.mako',
605 576 self._get_template_context(c), self.request)
606 577 html = formencode.htmlfill.render(
607 578 data,
608 579 defaults=defaults,
609 580 encoding="UTF-8",
610 581 force_defaults=False
611 582 )
612 583 return Response(html)
613 584
614 585 @LoginRequired()
615 586 @HasPermissionAllDecorator('hg.admin')
616 587 @CSRFRequired()
617 @view_config(
618 route_name='user_edit_global_perms_update', request_method='POST',
619 renderer='rhodecode:templates/admin/users/user_edit.mako')
620 588 def user_edit_global_perms_update(self):
621 589 _ = self.request.translate
622 590 c = self.load_default_context()
623 591
624 592 user_id = self.db_user_id
625 593 c.user = self.db_user
626 594
627 595 c.active = 'global_perms'
628 596 try:
629 597 # first stage that verifies the checkbox
630 598 _form = UserIndividualPermissionsForm(self.request.translate)
631 599 form_result = _form.to_python(dict(self.request.POST))
632 600 inherit_perms = form_result['inherit_default_permissions']
633 601 c.user.inherit_default_permissions = inherit_perms
634 602 Session().add(c.user)
635 603
636 604 if not inherit_perms:
637 605 # only update the individual ones if we un check the flag
638 606 _form = UserPermissionsForm(
639 607 self.request.translate,
640 608 [x[0] for x in c.repo_create_choices],
641 609 [x[0] for x in c.repo_create_on_write_choices],
642 610 [x[0] for x in c.repo_group_create_choices],
643 611 [x[0] for x in c.user_group_create_choices],
644 612 [x[0] for x in c.fork_choices],
645 613 [x[0] for x in c.inherit_default_permission_choices])()
646 614
647 615 form_result = _form.to_python(dict(self.request.POST))
648 616 form_result.update({'perm_user_id': c.user.user_id})
649 617
650 618 PermissionModel().update_user_permissions(form_result)
651 619
652 620 # TODO(marcink): implement global permissions
653 621 # audit_log.store_web('user.edit.permissions')
654 622
655 623 Session().commit()
656 624
657 625 h.flash(_('User global permissions updated successfully'),
658 626 category='success')
659 627
660 628 except formencode.Invalid as errors:
661 629 data = render(
662 630 'rhodecode:templates/admin/users/user_edit.mako',
663 631 self._get_template_context(c), self.request)
664 632 html = formencode.htmlfill.render(
665 633 data,
666 634 defaults=errors.value,
667 635 errors=errors.error_dict or {},
668 636 prefix_error=False,
669 637 encoding="UTF-8",
670 638 force_defaults=False
671 639 )
672 640 return Response(html)
673 641 except Exception:
674 642 log.exception("Exception during permissions saving")
675 643 h.flash(_('An error occurred during permissions saving'),
676 644 category='error')
677 645
678 646 affected_user_ids = [user_id]
679 647 PermissionModel().trigger_permission_flush(affected_user_ids)
680 648 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
681 649
682 650 @LoginRequired()
683 651 @HasPermissionAllDecorator('hg.admin')
684 652 @CSRFRequired()
685 @view_config(
686 route_name='user_enable_force_password_reset', request_method='POST',
687 renderer='rhodecode:templates/admin/users/user_edit.mako')
688 653 def user_enable_force_password_reset(self):
689 654 _ = self.request.translate
690 655 c = self.load_default_context()
691 656
692 657 user_id = self.db_user_id
693 658 c.user = self.db_user
694 659
695 660 try:
696 661 c.user.update_userdata(force_password_change=True)
697 662
698 663 msg = _('Force password change enabled for user')
699 664 audit_logger.store_web('user.edit.password_reset.enabled',
700 665 user=c.rhodecode_user)
701 666
702 667 Session().commit()
703 668 h.flash(msg, category='success')
704 669 except Exception:
705 670 log.exception("Exception during password reset for user")
706 671 h.flash(_('An error occurred during password reset for user'),
707 672 category='error')
708 673
709 674 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
710 675
711 676 @LoginRequired()
712 677 @HasPermissionAllDecorator('hg.admin')
713 678 @CSRFRequired()
714 @view_config(
715 route_name='user_disable_force_password_reset', request_method='POST',
716 renderer='rhodecode:templates/admin/users/user_edit.mako')
717 679 def user_disable_force_password_reset(self):
718 680 _ = self.request.translate
719 681 c = self.load_default_context()
720 682
721 683 user_id = self.db_user_id
722 684 c.user = self.db_user
723 685
724 686 try:
725 687 c.user.update_userdata(force_password_change=False)
726 688
727 689 msg = _('Force password change disabled for user')
728 690 audit_logger.store_web(
729 691 'user.edit.password_reset.disabled',
730 692 user=c.rhodecode_user)
731 693
732 694 Session().commit()
733 695 h.flash(msg, category='success')
734 696 except Exception:
735 697 log.exception("Exception during password reset for user")
736 698 h.flash(_('An error occurred during password reset for user'),
737 699 category='error')
738 700
739 701 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
740 702
741 703 @LoginRequired()
742 704 @HasPermissionAllDecorator('hg.admin')
743 705 @CSRFRequired()
744 @view_config(
745 route_name='user_notice_dismiss', request_method='POST',
746 renderer='json_ext', xhr=True)
747 706 def user_notice_dismiss(self):
748 707 _ = self.request.translate
749 708 c = self.load_default_context()
750 709
751 710 user_id = self.db_user_id
752 711 c.user = self.db_user
753 712 user_notice_id = safe_int(self.request.POST.get('notice_id'))
754 713 notice = UserNotice().query()\
755 714 .filter(UserNotice.user_id == user_id)\
756 715 .filter(UserNotice.user_notice_id == user_notice_id)\
757 716 .scalar()
758 717 read = False
759 718 if notice:
760 719 notice.notice_read = True
761 720 Session().add(notice)
762 721 Session().commit()
763 722 read = True
764 723
765 724 return {'notice': user_notice_id, 'read': read}
766 725
767 726 @LoginRequired()
768 727 @HasPermissionAllDecorator('hg.admin')
769 728 @CSRFRequired()
770 @view_config(
771 route_name='user_create_personal_repo_group', request_method='POST',
772 renderer='rhodecode:templates/admin/users/user_edit.mako')
773 729 def user_create_personal_repo_group(self):
774 730 """
775 731 Create personal repository group for this user
776 732 """
777 733 from rhodecode.model.repo_group import RepoGroupModel
778 734
779 735 _ = self.request.translate
780 736 c = self.load_default_context()
781 737
782 738 user_id = self.db_user_id
783 739 c.user = self.db_user
784 740
785 741 personal_repo_group = RepoGroup.get_user_personal_repo_group(
786 742 c.user.user_id)
787 743 if personal_repo_group:
788 744 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
789 745
790 746 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
791 747 named_personal_group = RepoGroup.get_by_group_name(
792 748 personal_repo_group_name)
793 749 try:
794 750
795 751 if named_personal_group and named_personal_group.user_id == c.user.user_id:
796 752 # migrate the same named group, and mark it as personal
797 753 named_personal_group.personal = True
798 754 Session().add(named_personal_group)
799 755 Session().commit()
800 756 msg = _('Linked repository group `%s` as personal' % (
801 757 personal_repo_group_name,))
802 758 h.flash(msg, category='success')
803 759 elif not named_personal_group:
804 760 RepoGroupModel().create_personal_repo_group(c.user)
805 761
806 762 msg = _('Created repository group `%s`' % (
807 763 personal_repo_group_name,))
808 764 h.flash(msg, category='success')
809 765 else:
810 766 msg = _('Repository group `%s` is already taken' % (
811 767 personal_repo_group_name,))
812 768 h.flash(msg, category='warning')
813 769 except Exception:
814 770 log.exception("Exception during repository group creation")
815 771 msg = _(
816 772 'An error occurred during repository group creation for user')
817 773 h.flash(msg, category='error')
818 774 Session().rollback()
819 775
820 776 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
821 777
822 778 @LoginRequired()
823 779 @HasPermissionAllDecorator('hg.admin')
824 @view_config(
825 route_name='edit_user_auth_tokens', request_method='GET',
826 renderer='rhodecode:templates/admin/users/user_edit.mako')
827 780 def auth_tokens(self):
828 781 _ = self.request.translate
829 782 c = self.load_default_context()
830 783 c.user = self.db_user
831 784
832 785 c.active = 'auth_tokens'
833 786
834 787 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
835 788 c.role_values = [
836 789 (x, AuthTokenModel.cls._get_role_name(x))
837 790 for x in AuthTokenModel.cls.ROLES]
838 791 c.role_options = [(c.role_values, _("Role"))]
839 792 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
840 793 c.user.user_id, show_expired=True)
841 794 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
842 795 return self._get_template_context(c)
843 796
844 797 @LoginRequired()
845 798 @HasPermissionAllDecorator('hg.admin')
846 @view_config(
847 route_name='edit_user_auth_tokens_view', request_method='POST',
848 renderer='json_ext', xhr=True)
849 799 def auth_tokens_view(self):
850 800 _ = self.request.translate
851 801 c = self.load_default_context()
852 802 c.user = self.db_user
853 803
854 804 auth_token_id = self.request.POST.get('auth_token_id')
855 805
856 806 if auth_token_id:
857 807 token = UserApiKeys.get_or_404(auth_token_id)
858 808
859 809 return {
860 810 'auth_token': token.api_key
861 811 }
862 812
863 813 def maybe_attach_token_scope(self, token):
864 814 # implemented in EE edition
865 815 pass
866 816
867 817 @LoginRequired()
868 818 @HasPermissionAllDecorator('hg.admin')
869 819 @CSRFRequired()
870 @view_config(
871 route_name='edit_user_auth_tokens_add', request_method='POST')
872 820 def auth_tokens_add(self):
873 821 _ = self.request.translate
874 822 c = self.load_default_context()
875 823
876 824 user_id = self.db_user_id
877 825 c.user = self.db_user
878 826
879 827 user_data = c.user.get_api_data()
880 828 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
881 829 description = self.request.POST.get('description')
882 830 role = self.request.POST.get('role')
883 831
884 832 token = UserModel().add_auth_token(
885 833 user=c.user.user_id,
886 834 lifetime_minutes=lifetime, role=role, description=description,
887 835 scope_callback=self.maybe_attach_token_scope)
888 836 token_data = token.get_api_data()
889 837
890 838 audit_logger.store_web(
891 839 'user.edit.token.add', action_data={
892 840 'data': {'token': token_data, 'user': user_data}},
893 841 user=self._rhodecode_user, )
894 842 Session().commit()
895 843
896 844 h.flash(_("Auth token successfully created"), category='success')
897 845 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
898 846
899 847 @LoginRequired()
900 848 @HasPermissionAllDecorator('hg.admin')
901 849 @CSRFRequired()
902 @view_config(
903 route_name='edit_user_auth_tokens_delete', request_method='POST')
904 850 def auth_tokens_delete(self):
905 851 _ = self.request.translate
906 852 c = self.load_default_context()
907 853
908 854 user_id = self.db_user_id
909 855 c.user = self.db_user
910 856
911 857 user_data = c.user.get_api_data()
912 858
913 859 del_auth_token = self.request.POST.get('del_auth_token')
914 860
915 861 if del_auth_token:
916 862 token = UserApiKeys.get_or_404(del_auth_token)
917 863 token_data = token.get_api_data()
918 864
919 865 AuthTokenModel().delete(del_auth_token, c.user.user_id)
920 866 audit_logger.store_web(
921 867 'user.edit.token.delete', action_data={
922 868 'data': {'token': token_data, 'user': user_data}},
923 869 user=self._rhodecode_user,)
924 870 Session().commit()
925 871 h.flash(_("Auth token successfully deleted"), category='success')
926 872
927 873 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
928 874
929 875 @LoginRequired()
930 876 @HasPermissionAllDecorator('hg.admin')
931 @view_config(
932 route_name='edit_user_ssh_keys', request_method='GET',
933 renderer='rhodecode:templates/admin/users/user_edit.mako')
934 877 def ssh_keys(self):
935 878 _ = self.request.translate
936 879 c = self.load_default_context()
937 880 c.user = self.db_user
938 881
939 882 c.active = 'ssh_keys'
940 883 c.default_key = self.request.GET.get('default_key')
941 884 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
942 885 return self._get_template_context(c)
943 886
944 887 @LoginRequired()
945 888 @HasPermissionAllDecorator('hg.admin')
946 @view_config(
947 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
948 renderer='rhodecode:templates/admin/users/user_edit.mako')
949 889 def ssh_keys_generate_keypair(self):
950 890 _ = self.request.translate
951 891 c = self.load_default_context()
952 892
953 893 c.user = self.db_user
954 894
955 895 c.active = 'ssh_keys_generate'
956 896 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
957 897 private_format = self.request.GET.get('private_format') \
958 898 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
959 899 c.private, c.public = SshKeyModel().generate_keypair(
960 900 comment=comment, private_format=private_format)
961 901
962 902 return self._get_template_context(c)
963 903
964 904 @LoginRequired()
965 905 @HasPermissionAllDecorator('hg.admin')
966 906 @CSRFRequired()
967 @view_config(
968 route_name='edit_user_ssh_keys_add', request_method='POST')
969 907 def ssh_keys_add(self):
970 908 _ = self.request.translate
971 909 c = self.load_default_context()
972 910
973 911 user_id = self.db_user_id
974 912 c.user = self.db_user
975 913
976 914 user_data = c.user.get_api_data()
977 915 key_data = self.request.POST.get('key_data')
978 916 description = self.request.POST.get('description')
979 917
980 918 fingerprint = 'unknown'
981 919 try:
982 920 if not key_data:
983 921 raise ValueError('Please add a valid public key')
984 922
985 923 key = SshKeyModel().parse_key(key_data.strip())
986 924 fingerprint = key.hash_md5()
987 925
988 926 ssh_key = SshKeyModel().create(
989 927 c.user.user_id, fingerprint, key.keydata, description)
990 928 ssh_key_data = ssh_key.get_api_data()
991 929
992 930 audit_logger.store_web(
993 931 'user.edit.ssh_key.add', action_data={
994 932 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
995 933 user=self._rhodecode_user, )
996 934 Session().commit()
997 935
998 936 # Trigger an event on change of keys.
999 937 trigger(SshKeyFileChangeEvent(), self.request.registry)
1000 938
1001 939 h.flash(_("Ssh Key successfully created"), category='success')
1002 940
1003 941 except IntegrityError:
1004 942 log.exception("Exception during ssh key saving")
1005 943 err = 'Such key with fingerprint `{}` already exists, ' \
1006 944 'please use a different one'.format(fingerprint)
1007 945 h.flash(_('An error occurred during ssh key saving: {}').format(err),
1008 946 category='error')
1009 947 except Exception as e:
1010 948 log.exception("Exception during ssh key saving")
1011 949 h.flash(_('An error occurred during ssh key saving: {}').format(e),
1012 950 category='error')
1013 951
1014 952 return HTTPFound(
1015 953 h.route_path('edit_user_ssh_keys', user_id=user_id))
1016 954
1017 955 @LoginRequired()
1018 956 @HasPermissionAllDecorator('hg.admin')
1019 957 @CSRFRequired()
1020 @view_config(
1021 route_name='edit_user_ssh_keys_delete', request_method='POST')
1022 958 def ssh_keys_delete(self):
1023 959 _ = self.request.translate
1024 960 c = self.load_default_context()
1025 961
1026 962 user_id = self.db_user_id
1027 963 c.user = self.db_user
1028 964
1029 965 user_data = c.user.get_api_data()
1030 966
1031 967 del_ssh_key = self.request.POST.get('del_ssh_key')
1032 968
1033 969 if del_ssh_key:
1034 970 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
1035 971 ssh_key_data = ssh_key.get_api_data()
1036 972
1037 973 SshKeyModel().delete(del_ssh_key, c.user.user_id)
1038 974 audit_logger.store_web(
1039 975 'user.edit.ssh_key.delete', action_data={
1040 976 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
1041 977 user=self._rhodecode_user,)
1042 978 Session().commit()
1043 979 # Trigger an event on change of keys.
1044 980 trigger(SshKeyFileChangeEvent(), self.request.registry)
1045 981 h.flash(_("Ssh key successfully deleted"), category='success')
1046 982
1047 983 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
1048 984
1049 985 @LoginRequired()
1050 986 @HasPermissionAllDecorator('hg.admin')
1051 @view_config(
1052 route_name='edit_user_emails', request_method='GET',
1053 renderer='rhodecode:templates/admin/users/user_edit.mako')
1054 987 def emails(self):
1055 988 _ = self.request.translate
1056 989 c = self.load_default_context()
1057 990 c.user = self.db_user
1058 991
1059 992 c.active = 'emails'
1060 993 c.user_email_map = UserEmailMap.query() \
1061 994 .filter(UserEmailMap.user == c.user).all()
1062 995
1063 996 return self._get_template_context(c)
1064 997
1065 998 @LoginRequired()
1066 999 @HasPermissionAllDecorator('hg.admin')
1067 1000 @CSRFRequired()
1068 @view_config(
1069 route_name='edit_user_emails_add', request_method='POST')
1070 1001 def emails_add(self):
1071 1002 _ = self.request.translate
1072 1003 c = self.load_default_context()
1073 1004
1074 1005 user_id = self.db_user_id
1075 1006 c.user = self.db_user
1076 1007
1077 1008 email = self.request.POST.get('new_email')
1078 1009 user_data = c.user.get_api_data()
1079 1010 try:
1080 1011
1081 1012 form = UserExtraEmailForm(self.request.translate)()
1082 1013 data = form.to_python({'email': email})
1083 1014 email = data['email']
1084 1015
1085 1016 UserModel().add_extra_email(c.user.user_id, email)
1086 1017 audit_logger.store_web(
1087 1018 'user.edit.email.add',
1088 1019 action_data={'email': email, 'user': user_data},
1089 1020 user=self._rhodecode_user)
1090 1021 Session().commit()
1091 1022 h.flash(_("Added new email address `%s` for user account") % email,
1092 1023 category='success')
1093 1024 except formencode.Invalid as error:
1094 1025 h.flash(h.escape(error.error_dict['email']), category='error')
1095 1026 except IntegrityError:
1096 1027 log.warning("Email %s already exists", email)
1097 1028 h.flash(_('Email `{}` is already registered for another user.').format(email),
1098 1029 category='error')
1099 1030 except Exception:
1100 1031 log.exception("Exception during email saving")
1101 1032 h.flash(_('An error occurred during email saving'),
1102 1033 category='error')
1103 1034 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1104 1035
1105 1036 @LoginRequired()
1106 1037 @HasPermissionAllDecorator('hg.admin')
1107 1038 @CSRFRequired()
1108 @view_config(
1109 route_name='edit_user_emails_delete', request_method='POST')
1110 1039 def emails_delete(self):
1111 1040 _ = self.request.translate
1112 1041 c = self.load_default_context()
1113 1042
1114 1043 user_id = self.db_user_id
1115 1044 c.user = self.db_user
1116 1045
1117 1046 email_id = self.request.POST.get('del_email_id')
1118 1047 user_model = UserModel()
1119 1048
1120 1049 email = UserEmailMap.query().get(email_id).email
1121 1050 user_data = c.user.get_api_data()
1122 1051 user_model.delete_extra_email(c.user.user_id, email_id)
1123 1052 audit_logger.store_web(
1124 1053 'user.edit.email.delete',
1125 1054 action_data={'email': email, 'user': user_data},
1126 1055 user=self._rhodecode_user)
1127 1056 Session().commit()
1128 1057 h.flash(_("Removed email address from user account"),
1129 1058 category='success')
1130 1059 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1131 1060
1132 1061 @LoginRequired()
1133 1062 @HasPermissionAllDecorator('hg.admin')
1134 @view_config(
1135 route_name='edit_user_ips', request_method='GET',
1136 renderer='rhodecode:templates/admin/users/user_edit.mako')
1137 1063 def ips(self):
1138 1064 _ = self.request.translate
1139 1065 c = self.load_default_context()
1140 1066 c.user = self.db_user
1141 1067
1142 1068 c.active = 'ips'
1143 1069 c.user_ip_map = UserIpMap.query() \
1144 1070 .filter(UserIpMap.user == c.user).all()
1145 1071
1146 1072 c.inherit_default_ips = c.user.inherit_default_permissions
1147 1073 c.default_user_ip_map = UserIpMap.query() \
1148 1074 .filter(UserIpMap.user == User.get_default_user()).all()
1149 1075
1150 1076 return self._get_template_context(c)
1151 1077
1152 1078 @LoginRequired()
1153 1079 @HasPermissionAllDecorator('hg.admin')
1154 1080 @CSRFRequired()
1155 @view_config(
1156 route_name='edit_user_ips_add', request_method='POST')
1157 1081 # NOTE(marcink): this view is allowed for default users, as we can
1158 1082 # edit their IP white list
1159 1083 def ips_add(self):
1160 1084 _ = self.request.translate
1161 1085 c = self.load_default_context()
1162 1086
1163 1087 user_id = self.db_user_id
1164 1088 c.user = self.db_user
1165 1089
1166 1090 user_model = UserModel()
1167 1091 desc = self.request.POST.get('description')
1168 1092 try:
1169 1093 ip_list = user_model.parse_ip_range(
1170 1094 self.request.POST.get('new_ip'))
1171 1095 except Exception as e:
1172 1096 ip_list = []
1173 1097 log.exception("Exception during ip saving")
1174 1098 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1175 1099 category='error')
1176 1100 added = []
1177 1101 user_data = c.user.get_api_data()
1178 1102 for ip in ip_list:
1179 1103 try:
1180 1104 form = UserExtraIpForm(self.request.translate)()
1181 1105 data = form.to_python({'ip': ip})
1182 1106 ip = data['ip']
1183 1107
1184 1108 user_model.add_extra_ip(c.user.user_id, ip, desc)
1185 1109 audit_logger.store_web(
1186 1110 'user.edit.ip.add',
1187 1111 action_data={'ip': ip, 'user': user_data},
1188 1112 user=self._rhodecode_user)
1189 1113 Session().commit()
1190 1114 added.append(ip)
1191 1115 except formencode.Invalid as error:
1192 1116 msg = error.error_dict['ip']
1193 1117 h.flash(msg, category='error')
1194 1118 except Exception:
1195 1119 log.exception("Exception during ip saving")
1196 1120 h.flash(_('An error occurred during ip saving'),
1197 1121 category='error')
1198 1122 if added:
1199 1123 h.flash(
1200 1124 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1201 1125 category='success')
1202 1126 if 'default_user' in self.request.POST:
1203 1127 # case for editing global IP list we do it for 'DEFAULT' user
1204 1128 raise HTTPFound(h.route_path('admin_permissions_ips'))
1205 1129 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1206 1130
1207 1131 @LoginRequired()
1208 1132 @HasPermissionAllDecorator('hg.admin')
1209 1133 @CSRFRequired()
1210 @view_config(
1211 route_name='edit_user_ips_delete', request_method='POST')
1212 1134 # NOTE(marcink): this view is allowed for default users, as we can
1213 1135 # edit their IP white list
1214 1136 def ips_delete(self):
1215 1137 _ = self.request.translate
1216 1138 c = self.load_default_context()
1217 1139
1218 1140 user_id = self.db_user_id
1219 1141 c.user = self.db_user
1220 1142
1221 1143 ip_id = self.request.POST.get('del_ip_id')
1222 1144 user_model = UserModel()
1223 1145 user_data = c.user.get_api_data()
1224 1146 ip = UserIpMap.query().get(ip_id).ip_addr
1225 1147 user_model.delete_extra_ip(c.user.user_id, ip_id)
1226 1148 audit_logger.store_web(
1227 1149 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1228 1150 user=self._rhodecode_user)
1229 1151 Session().commit()
1230 1152 h.flash(_("Removed ip address from user whitelist"), category='success')
1231 1153
1232 1154 if 'default_user' in self.request.POST:
1233 1155 # case for editing global IP list we do it for 'DEFAULT' user
1234 1156 raise HTTPFound(h.route_path('admin_permissions_ips'))
1235 1157 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1236 1158
1237 1159 @LoginRequired()
1238 1160 @HasPermissionAllDecorator('hg.admin')
1239 @view_config(
1240 route_name='edit_user_groups_management', request_method='GET',
1241 renderer='rhodecode:templates/admin/users/user_edit.mako')
1242 1161 def groups_management(self):
1243 1162 c = self.load_default_context()
1244 1163 c.user = self.db_user
1245 1164 c.data = c.user.group_member
1246 1165
1247 1166 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1248 1167 for group in c.user.group_member]
1249 1168 c.groups = json.dumps(groups)
1250 1169 c.active = 'groups'
1251 1170
1252 1171 return self._get_template_context(c)
1253 1172
1254 1173 @LoginRequired()
1255 1174 @HasPermissionAllDecorator('hg.admin')
1256 1175 @CSRFRequired()
1257 @view_config(
1258 route_name='edit_user_groups_management_updates', request_method='POST')
1259 1176 def groups_management_updates(self):
1260 1177 _ = self.request.translate
1261 1178 c = self.load_default_context()
1262 1179
1263 1180 user_id = self.db_user_id
1264 1181 c.user = self.db_user
1265 1182
1266 1183 user_groups = set(self.request.POST.getall('users_group_id'))
1267 1184 user_groups_objects = []
1268 1185
1269 1186 for ugid in user_groups:
1270 1187 user_groups_objects.append(
1271 1188 UserGroupModel().get_group(safe_int(ugid)))
1272 1189 user_group_model = UserGroupModel()
1273 1190 added_to_groups, removed_from_groups = \
1274 1191 user_group_model.change_groups(c.user, user_groups_objects)
1275 1192
1276 1193 user_data = c.user.get_api_data()
1277 1194 for user_group_id in added_to_groups:
1278 1195 user_group = UserGroup.get(user_group_id)
1279 1196 old_values = user_group.get_api_data()
1280 1197 audit_logger.store_web(
1281 1198 'user_group.edit.member.add',
1282 1199 action_data={'user': user_data, 'old_data': old_values},
1283 1200 user=self._rhodecode_user)
1284 1201
1285 1202 for user_group_id in removed_from_groups:
1286 1203 user_group = UserGroup.get(user_group_id)
1287 1204 old_values = user_group.get_api_data()
1288 1205 audit_logger.store_web(
1289 1206 'user_group.edit.member.delete',
1290 1207 action_data={'user': user_data, 'old_data': old_values},
1291 1208 user=self._rhodecode_user)
1292 1209
1293 1210 Session().commit()
1294 1211 c.active = 'user_groups_management'
1295 1212 h.flash(_("Groups successfully changed"), category='success')
1296 1213
1297 1214 return HTTPFound(h.route_path(
1298 1215 'edit_user_groups_management', user_id=user_id))
1299 1216
1300 1217 @LoginRequired()
1301 1218 @HasPermissionAllDecorator('hg.admin')
1302 @view_config(
1303 route_name='edit_user_audit_logs', request_method='GET',
1304 renderer='rhodecode:templates/admin/users/user_edit.mako')
1305 1219 def user_audit_logs(self):
1306 1220 _ = self.request.translate
1307 1221 c = self.load_default_context()
1308 1222 c.user = self.db_user
1309 1223
1310 1224 c.active = 'audit'
1311 1225
1312 1226 p = safe_int(self.request.GET.get('page', 1), 1)
1313 1227
1314 1228 filter_term = self.request.GET.get('filter')
1315 1229 user_log = UserModel().get_user_log(c.user, filter_term)
1316 1230
1317 1231 def url_generator(page_num):
1318 1232 query_params = {
1319 1233 'page': page_num
1320 1234 }
1321 1235 if filter_term:
1322 1236 query_params['filter'] = filter_term
1323 1237 return self.request.current_route_path(_query=query_params)
1324 1238
1325 1239 c.audit_logs = SqlPage(
1326 1240 user_log, page=p, items_per_page=10, url_maker=url_generator)
1327 1241 c.filter_term = filter_term
1328 1242 return self._get_template_context(c)
1329 1243
1330 1244 @LoginRequired()
1331 1245 @HasPermissionAllDecorator('hg.admin')
1332 @view_config(
1333 route_name='edit_user_audit_logs_download', request_method='GET',
1334 renderer='string')
1335 1246 def user_audit_logs_download(self):
1336 1247 _ = self.request.translate
1337 1248 c = self.load_default_context()
1338 1249 c.user = self.db_user
1339 1250
1340 1251 user_log = UserModel().get_user_log(c.user, filter_term=None)
1341 1252
1342 1253 audit_log_data = {}
1343 1254 for entry in user_log:
1344 1255 audit_log_data[entry.user_log_id] = entry.get_dict()
1345 1256
1346 1257 response = Response(json.dumps(audit_log_data, indent=4))
1347 1258 response.content_disposition = str(
1348 1259 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1349 1260 response.content_type = 'application/json'
1350 1261
1351 1262 return response
1352 1263
1353 1264 @LoginRequired()
1354 1265 @HasPermissionAllDecorator('hg.admin')
1355 @view_config(
1356 route_name='edit_user_perms_summary', request_method='GET',
1357 renderer='rhodecode:templates/admin/users/user_edit.mako')
1358 1266 def user_perms_summary(self):
1359 1267 _ = self.request.translate
1360 1268 c = self.load_default_context()
1361 1269 c.user = self.db_user
1362 1270
1363 1271 c.active = 'perms_summary'
1364 1272 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1365 1273
1366 1274 return self._get_template_context(c)
1367 1275
1368 1276 @LoginRequired()
1369 1277 @HasPermissionAllDecorator('hg.admin')
1370 @view_config(
1371 route_name='edit_user_perms_summary_json', request_method='GET',
1372 renderer='json_ext')
1373 1278 def user_perms_summary_json(self):
1374 1279 self.load_default_context()
1375 1280 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1376 1281
1377 1282 return perm_user.permissions
1378 1283
1379 1284 @LoginRequired()
1380 1285 @HasPermissionAllDecorator('hg.admin')
1381 @view_config(
1382 route_name='edit_user_caches', request_method='GET',
1383 renderer='rhodecode:templates/admin/users/user_edit.mako')
1384 1286 def user_caches(self):
1385 1287 _ = self.request.translate
1386 1288 c = self.load_default_context()
1387 1289 c.user = self.db_user
1388 1290
1389 1291 c.active = 'caches'
1390 1292 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1391 1293
1392 1294 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1393 1295 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1394 1296 c.backend = c.region.backend
1395 1297 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1396 1298
1397 1299 return self._get_template_context(c)
1398 1300
1399 1301 @LoginRequired()
1400 1302 @HasPermissionAllDecorator('hg.admin')
1401 1303 @CSRFRequired()
1402 @view_config(
1403 route_name='edit_user_caches_update', request_method='POST')
1404 1304 def user_caches_update(self):
1405 1305 _ = self.request.translate
1406 1306 c = self.load_default_context()
1407 1307 c.user = self.db_user
1408 1308
1409 1309 c.active = 'caches'
1410 1310 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1411 1311
1412 1312 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1413 1313 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1414 1314
1415 1315 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1416 1316
1417 1317 return HTTPFound(h.route_path(
1418 1318 'edit_user_caches', user_id=c.user.user_id))
@@ -1,96 +1,106 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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
23 23 from pyramid.events import ApplicationCreated
24 24 from pyramid.settings import asbool
25 25
26 26 from rhodecode.apps._base import ADMIN_PREFIX
27 27 from rhodecode.lib.ext_json import json
28 28
29 29
30 30 def url_gen(request):
31 31 registry = request.registry
32 32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
33 33 ws_url = registry.settings.get('channelstream.ws_url', '')
34 34 proxy_url = request.route_url('channelstream_proxy')
35 35 urls = {
36 36 'connect': request.route_path('channelstream_connect'),
37 37 'subscribe': request.route_path('channelstream_subscribe'),
38 38 'longpoll': longpoll_url or proxy_url,
39 39 'ws': ws_url or proxy_url.replace('http', 'ws')
40 40 }
41 41 return json.dumps(urls)
42 42
43 43
44 44 PLUGIN_DEFINITION = {
45 45 'name': 'channelstream',
46 46 'config': {
47 47 'javascript': [],
48 48 'css': [],
49 49 'template_hooks': {
50 50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
51 51 },
52 52 'url_gen': url_gen,
53 53 'static': None,
54 54 'enabled': False,
55 55 'server': '',
56 56 'secret': ''
57 57 }
58 58 }
59 59
60 60
61 61 def maybe_create_history_store(event):
62 62 # create plugin history location
63 63 settings = event.app.registry.settings
64 64 history_dir = settings.get('channelstream.history.location', '')
65 65 if history_dir and not os.path.exists(history_dir):
66 66 os.makedirs(history_dir, 0o750)
67 67
68 68
69 69 def includeme(config):
70 from rhodecode.apps.channelstream.views import ChannelstreamView
71
70 72 settings = config.registry.settings
71 73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
72 74 settings.get('channelstream.enabled'))
73 75 PLUGIN_DEFINITION['config']['server'] = settings.get(
74 76 'channelstream.server', '')
75 77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
76 78 'channelstream.secret', '')
77 79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
78 80 'channelstream.history.location', '')
79 81 config.register_rhodecode_plugin(
80 82 PLUGIN_DEFINITION['name'],
81 83 PLUGIN_DEFINITION['config']
82 84 )
83 85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
84 86
85 87 config.add_route(
86 88 name='channelstream_connect',
87 89 pattern=ADMIN_PREFIX + '/channelstream/connect')
90 config.add_view(
91 ChannelstreamView,
92 attr='channelstream_connect',
93 route_name='channelstream_connect', renderer='json_ext')
94
88 95 config.add_route(
89 96 name='channelstream_subscribe',
90 97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
98 config.add_view(
99 ChannelstreamView,
100 attr='channelstream_subscribe',
101 route_name='channelstream_subscribe', renderer='json_ext')
102
91 103 config.add_route(
92 104 name='channelstream_proxy',
93 105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
94 106
95 # Scan module for configuration decorators.
96 config.scan('.views', ignore='.tests')
@@ -1,187 +1,185 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 uuid
23 23
24 from pyramid.view import view_config
24
25 25 from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib.channelstream import (
29 29 channelstream_request, get_channelstream_server_url,
30 30 ChannelstreamConnectionException,
31 31 ChannelstreamPermissionException,
32 32 check_channel_permissions,
33 33 get_connection_validators,
34 34 get_user_data,
35 35 parse_channels_info,
36 36 update_history_from_logs,
37 37 USER_STATE_PUBLIC_KEYS)
38 38
39 39 from rhodecode.lib.auth import NotAnonymous
40 40
41 41 log = logging.getLogger(__name__)
42 42
43 43
44 44 class ChannelstreamView(BaseAppView):
45 45
46 46 def load_default_context(self):
47 47 c = self._get_local_tmpl_context()
48 48 self.channelstream_config = \
49 49 self.request.registry.rhodecode_plugins['channelstream']
50 50 if not self.channelstream_config.get('enabled'):
51 51 log.error('Channelstream plugin is disabled')
52 52 raise HTTPBadRequest()
53 53
54 54 return c
55 55
56 56 @NotAnonymous()
57 @view_config(route_name='channelstream_connect', renderer='json_ext')
58 def connect(self):
57 def channelstream_connect(self):
59 58 """ handle authorization of users trying to connect """
60 59
61 60 self.load_default_context()
62 61 try:
63 62 json_body = self.request.json_body
64 63 except Exception:
65 64 log.exception('Failed to decode json from request')
66 65 raise HTTPBadRequest()
67 66
68 67 try:
69 68 channels = check_channel_permissions(
70 69 json_body.get('channels'),
71 70 get_connection_validators(self.request.registry))
72 71 except ChannelstreamPermissionException:
73 72 log.error('Incorrect permissions for requested channels')
74 73 raise HTTPForbidden()
75 74
76 75 user = self._rhodecode_user
77 76 if user.user_id:
78 77 user_data = get_user_data(user.user_id)
79 78 else:
80 79 user_data = {
81 80 'id': None,
82 81 'username': None,
83 82 'first_name': None,
84 83 'last_name': None,
85 84 'icon_link': None,
86 85 'display_name': None,
87 86 'display_link': None,
88 87 }
89 88
90 89 #user_data['permissions'] = self._rhodecode_user.permissions_safe
91 90
92 91 payload = {
93 92 'username': user.username,
94 93 'user_state': user_data,
95 94 'conn_id': str(uuid.uuid4()),
96 95 'channels': channels,
97 96 'channel_configs': {},
98 97 'state_public_keys': USER_STATE_PUBLIC_KEYS,
99 98 'info': {
100 99 'exclude_channels': ['broadcast']
101 100 }
102 101 }
103 102 filtered_channels = [channel for channel in channels
104 103 if channel != 'broadcast']
105 104 for channel in filtered_channels:
106 105 payload['channel_configs'][channel] = {
107 106 'notify_presence': True,
108 107 'history_size': 100,
109 108 'store_history': True,
110 109 'broadcast_presence_with_user_lists': True
111 110 }
112 111 # connect user to server
113 112 channelstream_url = get_channelstream_server_url(
114 113 self.channelstream_config, '/connect')
115 114 try:
116 115 connect_result = channelstream_request(
117 116 self.channelstream_config, payload, '/connect')
118 117 except ChannelstreamConnectionException:
119 118 log.exception(
120 119 'Channelstream service at {} is down'.format(channelstream_url))
121 120 return HTTPBadGateway()
122 121
123 122 channel_info = connect_result.get('channels_info')
124 123 if not channel_info:
125 124 raise HTTPBadRequest()
126 125
127 126 connect_result['channels'] = channels
128 127 connect_result['channels_info'] = parse_channels_info(
129 128 channel_info, include_channel_info=filtered_channels)
130 129 update_history_from_logs(self.channelstream_config,
131 130 filtered_channels, connect_result)
132 131 return connect_result
133 132
134 133 @NotAnonymous()
135 @view_config(route_name='channelstream_subscribe', renderer='json_ext')
136 def subscribe(self):
134 def channelstream_subscribe(self):
137 135 """ can be used to subscribe specific connection to other channels """
138 136 self.load_default_context()
139 137 try:
140 138 json_body = self.request.json_body
141 139 except Exception:
142 140 log.exception('Failed to decode json from request')
143 141 raise HTTPBadRequest()
144 142 try:
145 143 channels = check_channel_permissions(
146 144 json_body.get('channels'),
147 145 get_connection_validators(self.request.registry))
148 146 except ChannelstreamPermissionException:
149 147 log.error('Incorrect permissions for requested channels')
150 148 raise HTTPForbidden()
151 149 payload = {'conn_id': json_body.get('conn_id', ''),
152 150 'channels': channels,
153 151 'channel_configs': {},
154 152 'info': {
155 153 'exclude_channels': ['broadcast']}
156 154 }
157 155 filtered_channels = [chan for chan in channels if chan != 'broadcast']
158 156 for channel in filtered_channels:
159 157 payload['channel_configs'][channel] = {
160 158 'notify_presence': True,
161 159 'history_size': 100,
162 160 'store_history': True,
163 161 'broadcast_presence_with_user_lists': True
164 162 }
165 163
166 164 channelstream_url = get_channelstream_server_url(
167 165 self.channelstream_config, '/subscribe')
168 166 try:
169 167 connect_result = channelstream_request(
170 168 self.channelstream_config, payload, '/subscribe')
171 169 except ChannelstreamConnectionException:
172 170 log.exception(
173 171 'Channelstream service at {} is down'.format(channelstream_url))
174 172 return HTTPBadGateway()
175 173
176 174 channel_info = connect_result.get('channels_info')
177 175 if not channel_info:
178 176 raise HTTPBadRequest()
179 177
180 178 # include_channel_info will limit history only to new channel
181 179 # to not overwrite histories on other channels in client
182 180 connect_result['channels_info'] = parse_channels_info(
183 181 channel_info,
184 182 include_channel_info=filtered_channels)
185 183 update_history_from_logs(
186 184 self.channelstream_config, filtered_channels, connect_result)
187 185 return connect_result
@@ -1,59 +1,81 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
21 21 from rhodecode.lib.utils2 import str2bool
22 22
23 23
24 24 class DebugStylePredicate(object):
25 25 def __init__(self, val, config):
26 26 self.val = val
27 27
28 28 def text(self):
29 29 return 'debug style route = %s' % self.val
30 30
31 31 phash = text
32 32
33 33 def __call__(self, info, request):
34 34 return str2bool(request.registry.settings.get('debug_style'))
35 35
36 36
37 37 def includeme(config):
38 from rhodecode.apps.debug_style.views import DebugStyleView
39
38 40 config.add_route_predicate(
39 41 'debug_style', DebugStylePredicate)
40 42
41 43 config.add_route(
42 44 name='debug_style_home',
43 45 pattern=ADMIN_PREFIX + '/debug_style',
44 46 debug_style=True)
47 config.add_view(
48 DebugStyleView,
49 attr='index',
50 route_name='debug_style_home', request_method='GET',
51 renderer=None)
52
45 53 config.add_route(
46 54 name='debug_style_email',
47 55 pattern=ADMIN_PREFIX + '/debug_style/email/{email_id}',
48 56 debug_style=True)
57 config.add_view(
58 DebugStyleView,
59 attr='render_email',
60 route_name='debug_style_email', request_method='GET',
61 renderer=None)
62
49 63 config.add_route(
50 64 name='debug_style_email_plain_rendered',
51 65 pattern=ADMIN_PREFIX + '/debug_style/email-rendered/{email_id}',
52 66 debug_style=True)
67 config.add_view(
68 DebugStyleView,
69 attr='render_email',
70 route_name='debug_style_email_plain_rendered', request_method='GET',
71 renderer=None)
72
53 73 config.add_route(
54 74 name='debug_style_template',
55 75 pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',
56 76 debug_style=True)
57
58 # Scan module for configuration decorators.
59 config.scan('.views', ignore='.tests')
77 config.add_view(
78 DebugStyleView,
79 attr='template',
80 route_name='debug_style_template', request_method='GET',
81 renderer=None)
@@ -1,486 +1,471 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 logging
23 23 import datetime
24 24
25 from pyramid.view import view_config
26 25 from pyramid.renderers import render_to_response
27 26 from rhodecode.apps._base import BaseAppView
28 27 from rhodecode.lib.celerylib import run_task, tasks
29 28 from rhodecode.lib.utils2 import AttributeDict
30 29 from rhodecode.model.db import User
31 30 from rhodecode.model.notification import EmailNotificationModel
32 31
33 32 log = logging.getLogger(__name__)
34 33
35 34
36 35 class DebugStyleView(BaseAppView):
37 36
38 37 def load_default_context(self):
39 38 c = self._get_local_tmpl_context()
40
41 39 return c
42 40
43 @view_config(
44 route_name='debug_style_home', request_method='GET',
45 renderer=None)
46 41 def index(self):
47 42 c = self.load_default_context()
48 43 c.active = 'index'
49 44
50 45 return render_to_response(
51 46 'debug_style/index.html', self._get_template_context(c),
52 47 request=self.request)
53 48
54 @view_config(
55 route_name='debug_style_email', request_method='GET',
56 renderer=None)
57 @view_config(
58 route_name='debug_style_email_plain_rendered', request_method='GET',
59 renderer=None)
60 49 def render_email(self):
61 50 c = self.load_default_context()
62 51 email_id = self.request.matchdict['email_id']
63 52 c.active = 'emails'
64 53
65 54 pr = AttributeDict(
66 55 pull_request_id=123,
67 56 title='digital_ocean: fix redis, elastic search start on boot, '
68 57 'fix fd limits on supervisor, set postgres 11 version',
69 58 description='''
70 59 Check if we should use full-topic or mini-topic.
71 60
72 61 - full topic produces some problems with merge states etc
73 62 - server-mini-topic needs probably tweeks.
74 63 ''',
75 64 repo_name='foobar',
76 65 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
77 66 target_ref_parts=AttributeDict(type='branch', name='master'),
78 67 )
79 68
80 69 target_repo = AttributeDict(repo_name='repo_group/target_repo')
81 70 source_repo = AttributeDict(repo_name='repo_group/source_repo')
82 71 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
83 72 # file/commit changes for PR update
84 73 commit_changes = AttributeDict({
85 74 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
86 75 'removed': ['eeeeeeeeeee'],
87 76 })
88 77
89 78 file_changes = AttributeDict({
90 79 'added': ['a/file1.md', 'file2.py'],
91 80 'modified': ['b/modified_file.rst'],
92 81 'removed': ['.idea'],
93 82 })
94 83
95 84 exc_traceback = {
96 85 'exc_utc_date': '2020-03-26T12:54:50.683281',
97 86 'exc_id': 139638856342656,
98 87 'exc_timestamp': '1585227290.683288',
99 88 'version': 'v1',
100 89 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
101 90 'exc_type': 'AttributeError'
102 91 }
103 92
104 93 email_kwargs = {
105 94 'test': {},
106 95
107 96 'message': {
108 97 'body': 'message body !'
109 98 },
110 99
111 100 'email_test': {
112 101 'user': user,
113 102 'date': datetime.datetime.now(),
114 103 },
115 104
116 105 'exception': {
117 106 'email_prefix': '[RHODECODE ERROR]',
118 107 'exc_id': exc_traceback['exc_id'],
119 108 'exc_url': 'http://server-url/{}'.format(exc_traceback['exc_id']),
120 109 'exc_type_name': 'NameError',
121 110 'exc_traceback': exc_traceback,
122 111 },
123 112
124 113 'password_reset': {
125 114 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
126 115
127 116 'user': user,
128 117 'date': datetime.datetime.now(),
129 118 'email': 'test@rhodecode.com',
130 119 'first_admin_email': User.get_first_super_admin().email
131 120 },
132 121
133 122 'password_reset_confirmation': {
134 123 'new_password': 'new-password-example',
135 124 'user': user,
136 125 'date': datetime.datetime.now(),
137 126 'email': 'test@rhodecode.com',
138 127 'first_admin_email': User.get_first_super_admin().email
139 128 },
140 129
141 130 'registration': {
142 131 'user': user,
143 132 'date': datetime.datetime.now(),
144 133 },
145 134
146 135 'pull_request_comment': {
147 136 'user': user,
148 137
149 138 'status_change': None,
150 139 'status_change_type': None,
151 140
152 141 'pull_request': pr,
153 142 'pull_request_commits': [],
154 143
155 144 'pull_request_target_repo': target_repo,
156 145 'pull_request_target_repo_url': 'http://target-repo/url',
157 146
158 147 'pull_request_source_repo': source_repo,
159 148 'pull_request_source_repo_url': 'http://source-repo/url',
160 149
161 150 'pull_request_url': 'http://localhost/pr1',
162 151 'pr_comment_url': 'http://comment-url',
163 152 'pr_comment_reply_url': 'http://comment-url#reply',
164 153
165 154 'comment_file': None,
166 155 'comment_line': None,
167 156 'comment_type': 'note',
168 157 'comment_body': 'This is my comment body. *I like !*',
169 158 'comment_id': 2048,
170 159 'renderer_type': 'markdown',
171 160 'mention': True,
172 161
173 162 },
174 163
175 164 'pull_request_comment+status': {
176 165 'user': user,
177 166
178 167 'status_change': 'approved',
179 168 'status_change_type': 'approved',
180 169
181 170 'pull_request': pr,
182 171 'pull_request_commits': [],
183 172
184 173 'pull_request_target_repo': target_repo,
185 174 'pull_request_target_repo_url': 'http://target-repo/url',
186 175
187 176 'pull_request_source_repo': source_repo,
188 177 'pull_request_source_repo_url': 'http://source-repo/url',
189 178
190 179 'pull_request_url': 'http://localhost/pr1',
191 180 'pr_comment_url': 'http://comment-url',
192 181 'pr_comment_reply_url': 'http://comment-url#reply',
193 182
194 183 'comment_type': 'todo',
195 184 'comment_file': None,
196 185 'comment_line': None,
197 186 'comment_body': '''
198 187 I think something like this would be better
199 188
200 189 ```py
201 190 // markdown renderer
202 191
203 192 def db():
204 193 global connection
205 194 return connection
206 195
207 196 ```
208 197
209 198 ''',
210 199 'comment_id': 2048,
211 200 'renderer_type': 'markdown',
212 201 'mention': True,
213 202
214 203 },
215 204
216 205 'pull_request_comment+file': {
217 206 'user': user,
218 207
219 208 'status_change': None,
220 209 'status_change_type': None,
221 210
222 211 'pull_request': pr,
223 212 'pull_request_commits': [],
224 213
225 214 'pull_request_target_repo': target_repo,
226 215 'pull_request_target_repo_url': 'http://target-repo/url',
227 216
228 217 'pull_request_source_repo': source_repo,
229 218 'pull_request_source_repo_url': 'http://source-repo/url',
230 219
231 220 'pull_request_url': 'http://localhost/pr1',
232 221
233 222 'pr_comment_url': 'http://comment-url',
234 223 'pr_comment_reply_url': 'http://comment-url#reply',
235 224
236 225 'comment_file': 'rhodecode/model/get_flow_commits',
237 226 'comment_line': 'o1210',
238 227 'comment_type': 'todo',
239 228 'comment_body': '''
240 229 I like this !
241 230
242 231 But please check this code
243 232
244 233 .. code-block:: javascript
245 234
246 235 // THIS IS RST CODE
247 236
248 237 this.createResolutionComment = function(commentId) {
249 238 // hide the trigger text
250 239 $('#resolve-comment-{0}'.format(commentId)).hide();
251 240
252 241 var comment = $('#comment-'+commentId);
253 242 var commentData = comment.data();
254 243 if (commentData.commentInline) {
255 244 this.createComment(comment, f_path, line_no, commentId)
256 245 } else {
257 246 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
258 247 }
259 248
260 249 return false;
261 250 };
262 251
263 252 This should work better !
264 253 ''',
265 254 'comment_id': 2048,
266 255 'renderer_type': 'rst',
267 256 'mention': True,
268 257
269 258 },
270 259
271 260 'pull_request_update': {
272 261 'updating_user': user,
273 262
274 263 'status_change': None,
275 264 'status_change_type': None,
276 265
277 266 'pull_request': pr,
278 267 'pull_request_commits': [],
279 268
280 269 'pull_request_target_repo': target_repo,
281 270 'pull_request_target_repo_url': 'http://target-repo/url',
282 271
283 272 'pull_request_source_repo': source_repo,
284 273 'pull_request_source_repo_url': 'http://source-repo/url',
285 274
286 275 'pull_request_url': 'http://localhost/pr1',
287 276
288 277 # update comment links
289 278 'pr_comment_url': 'http://comment-url',
290 279 'pr_comment_reply_url': 'http://comment-url#reply',
291 280 'ancestor_commit_id': 'f39bd443',
292 281 'added_commits': commit_changes.added,
293 282 'removed_commits': commit_changes.removed,
294 283 'changed_files': (file_changes.added + file_changes.modified + file_changes.removed),
295 284 'added_files': file_changes.added,
296 285 'modified_files': file_changes.modified,
297 286 'removed_files': file_changes.removed,
298 287 },
299 288
300 289 'cs_comment': {
301 290 'user': user,
302 291 'commit': AttributeDict(idx=123, raw_id='a'*40, message='Commit message'),
303 292 'status_change': None,
304 293 'status_change_type': None,
305 294
306 295 'commit_target_repo_url': 'http://foo.example.com/#comment1',
307 296 'repo_name': 'test-repo',
308 297 'comment_type': 'note',
309 298 'comment_file': None,
310 299 'comment_line': None,
311 300 'commit_comment_url': 'http://comment-url',
312 301 'commit_comment_reply_url': 'http://comment-url#reply',
313 302 'comment_body': 'This is my comment body. *I like !*',
314 303 'comment_id': 2048,
315 304 'renderer_type': 'markdown',
316 305 'mention': True,
317 306 },
318 307
319 308 'cs_comment+status': {
320 309 'user': user,
321 310 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
322 311 'status_change': 'approved',
323 312 'status_change_type': 'approved',
324 313
325 314 'commit_target_repo_url': 'http://foo.example.com/#comment1',
326 315 'repo_name': 'test-repo',
327 316 'comment_type': 'note',
328 317 'comment_file': None,
329 318 'comment_line': None,
330 319 'commit_comment_url': 'http://comment-url',
331 320 'commit_comment_reply_url': 'http://comment-url#reply',
332 321 'comment_body': '''
333 322 Hello **world**
334 323
335 324 This is a multiline comment :)
336 325
337 326 - list
338 327 - list2
339 328 ''',
340 329 'comment_id': 2048,
341 330 'renderer_type': 'markdown',
342 331 'mention': True,
343 332 },
344 333
345 334 'cs_comment+file': {
346 335 'user': user,
347 336 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
348 337 'status_change': None,
349 338 'status_change_type': None,
350 339
351 340 'commit_target_repo_url': 'http://foo.example.com/#comment1',
352 341 'repo_name': 'test-repo',
353 342
354 343 'comment_type': 'note',
355 344 'comment_file': 'test-file.py',
356 345 'comment_line': 'n100',
357 346
358 347 'commit_comment_url': 'http://comment-url',
359 348 'commit_comment_reply_url': 'http://comment-url#reply',
360 349 'comment_body': 'This is my comment body. *I like !*',
361 350 'comment_id': 2048,
362 351 'renderer_type': 'markdown',
363 352 'mention': True,
364 353 },
365 354
366 355 'pull_request': {
367 356 'user': user,
368 357 'pull_request': pr,
369 358 'pull_request_commits': [
370 359 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
371 360 my-account: moved email closer to profile as it's similar data just moved outside.
372 361 '''),
373 362 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
374 363 users: description edit fixes
375 364
376 365 - tests
377 366 - added metatags info
378 367 '''),
379 368 ],
380 369
381 370 'pull_request_target_repo': target_repo,
382 371 'pull_request_target_repo_url': 'http://target-repo/url',
383 372
384 373 'pull_request_source_repo': source_repo,
385 374 'pull_request_source_repo_url': 'http://source-repo/url',
386 375
387 376 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 377 'user_role': 'reviewer',
389 378 },
390 379
391 380 'pull_request+reviewer_role': {
392 381 'user': user,
393 382 'pull_request': pr,
394 383 'pull_request_commits': [
395 384 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
396 385 my-account: moved email closer to profile as it's similar data just moved outside.
397 386 '''),
398 387 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
399 388 users: description edit fixes
400 389
401 390 - tests
402 391 - added metatags info
403 392 '''),
404 393 ],
405 394
406 395 'pull_request_target_repo': target_repo,
407 396 'pull_request_target_repo_url': 'http://target-repo/url',
408 397
409 398 'pull_request_source_repo': source_repo,
410 399 'pull_request_source_repo_url': 'http://source-repo/url',
411 400
412 401 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 402 'user_role': 'reviewer',
414 403 },
415 404
416 405 'pull_request+observer_role': {
417 406 'user': user,
418 407 'pull_request': pr,
419 408 'pull_request_commits': [
420 409 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 410 my-account: moved email closer to profile as it's similar data just moved outside.
422 411 '''),
423 412 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 413 users: description edit fixes
425 414
426 415 - tests
427 416 - added metatags info
428 417 '''),
429 418 ],
430 419
431 420 'pull_request_target_repo': target_repo,
432 421 'pull_request_target_repo_url': 'http://target-repo/url',
433 422
434 423 'pull_request_source_repo': source_repo,
435 424 'pull_request_source_repo_url': 'http://source-repo/url',
436 425
437 426 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 427 'user_role': 'observer'
439 428 }
440 429 }
441 430
442 431 template_type = email_id.split('+')[0]
443 432 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
444 433 template_type, **email_kwargs.get(email_id, {}))
445 434
446 435 test_email = self.request.GET.get('email')
447 436 if test_email:
448 437 recipients = [test_email]
449 438 run_task(tasks.send_email, recipients, c.subject,
450 439 c.email_body_plaintext, c.email_body)
451 440
452 441 if self.request.matched_route.name == 'debug_style_email_plain_rendered':
453 442 template = 'debug_style/email_plain_rendered.mako'
454 443 else:
455 444 template = 'debug_style/email.mako'
456 445 return render_to_response(
457 446 template, self._get_template_context(c),
458 447 request=self.request)
459 448
460 @view_config(
461 route_name='debug_style_template', request_method='GET',
462 renderer=None)
463 449 def template(self):
464 450 t_path = self.request.matchdict['t_path']
465 451 c = self.load_default_context()
466 452 c.active = os.path.splitext(t_path)[0]
467 453 c.came_from = ''
468 454 # NOTE(marcink): extend the email types with variations based on data sets
469 455 c.email_types = {
470 456 'cs_comment+file': {},
471 457 'cs_comment+status': {},
472 458
473 459 'pull_request_comment+file': {},
474 460 'pull_request_comment+status': {},
475 461
476 462 'pull_request_update': {},
477 463
478 464 'pull_request+reviewer_role': {},
479 465 'pull_request+observer_role': {},
480 466 }
481 467 c.email_types.update(EmailNotificationModel.email_types)
482 468
483 469 return render_to_response(
484 470 'debug_style/' + t_path, self._get_template_context(c),
485 471 request=self.request)
486
@@ -1,52 +1,65 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import os
21 21 from rhodecode.apps.file_store import config_keys
22 22 from rhodecode.config.middleware import _bool_setting, _string_setting
23 23
24 24
25 25 def _sanitize_settings_and_apply_defaults(settings):
26 26 """
27 27 Set defaults, convert to python types and validate settings.
28 28 """
29 29 _bool_setting(settings, config_keys.enabled, 'true')
30 30
31 31 _string_setting(settings, config_keys.backend, 'local')
32 32
33 33 default_store = os.path.join(os.path.dirname(settings['__file__']), 'upload_store')
34 34 _string_setting(settings, config_keys.store_path, default_store)
35 35
36 36
37 37 def includeme(config):
38 from rhodecode.apps.file_store.views import FileStoreView
39
38 40 settings = config.registry.settings
39 41 _sanitize_settings_and_apply_defaults(settings)
40 42
41 43 config.add_route(
42 44 name='upload_file',
43 45 pattern='/_file_store/upload')
46 config.add_view(
47 FileStoreView,
48 attr='upload_file',
49 route_name='upload_file', request_method='POST', renderer='json_ext')
50
44 51 config.add_route(
45 52 name='download_file',
46 53 pattern='/_file_store/download/{fid:.*}')
54 config.add_view(
55 FileStoreView,
56 attr='download_file',
57 route_name='download_file')
58
47 59 config.add_route(
48 60 name='download_file_by_token',
49 61 pattern='/_file_store/token-download/{_auth_token}/{fid:.*}')
50
51 # Scan module for configuration decorators.
52 config.scan('.views', ignore='.tests')
62 config.add_view(
63 FileStoreView,
64 attr='download_file_by_token',
65 route_name='download_file_by_token')
@@ -1,205 +1,202 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import logging
21 21
22 from pyramid.view import view_config
22
23 23 from pyramid.response import FileResponse
24 24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.exceptions import (
29 29 FileNotAllowedException, FileOverSizeException)
30 30
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib import audit_logger
33 33 from rhodecode.lib.auth import (
34 34 CSRFRequired, NotAnonymous, HasRepoPermissionAny, HasRepoGroupPermissionAny,
35 35 LoginRequired)
36 36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 37 from rhodecode.model.db import Session, FileStore, UserApiKeys
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class FileStoreView(BaseAppView):
43 43 upload_key = 'store_file'
44 44
45 45 def load_default_context(self):
46 46 c = self._get_local_tmpl_context()
47 47 self.storage = utils.get_file_storage(self.request.registry.settings)
48 48 return c
49 49
50 50 def _guess_type(self, file_name):
51 51 """
52 52 Our own type guesser for mimetypes using the rich DB
53 53 """
54 54 if not hasattr(self, 'db'):
55 55 self.db = get_mimetypes_db()
56 56 _content_type, _encoding = self.db.guess_type(file_name, strict=False)
57 57 return _content_type, _encoding
58 58
59 59 def _serve_file(self, file_uid):
60 60 if not self.storage.exists(file_uid):
61 61 store_path = self.storage.store_path(file_uid)
62 62 log.debug('File with FID:%s not found in the store under `%s`',
63 63 file_uid, store_path)
64 64 raise HTTPNotFound()
65 65
66 66 db_obj = FileStore.get_by_store_uid(file_uid, safe=True)
67 67 if not db_obj:
68 68 raise HTTPNotFound()
69 69
70 70 # private upload for user
71 71 if db_obj.check_acl and db_obj.scope_user_id:
72 72 log.debug('Artifact: checking scope access for bound artifact user: `%s`',
73 73 db_obj.scope_user_id)
74 74 user = db_obj.user
75 75 if self._rhodecode_db_user.user_id != user.user_id:
76 76 log.warning('Access to file store object forbidden')
77 77 raise HTTPNotFound()
78 78
79 79 # scoped to repository permissions
80 80 if db_obj.check_acl and db_obj.scope_repo_id:
81 81 log.debug('Artifact: checking scope access for bound artifact repo: `%s`',
82 82 db_obj.scope_repo_id)
83 83 repo = db_obj.repo
84 84 perm_set = ['repository.read', 'repository.write', 'repository.admin']
85 85 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'FileStore check')
86 86 if not has_perm:
87 87 log.warning('Access to file store object `%s` forbidden', file_uid)
88 88 raise HTTPNotFound()
89 89
90 90 # scoped to repository group permissions
91 91 if db_obj.check_acl and db_obj.scope_repo_group_id:
92 92 log.debug('Artifact: checking scope access for bound artifact repo group: `%s`',
93 93 db_obj.scope_repo_group_id)
94 94 repo_group = db_obj.repo_group
95 95 perm_set = ['group.read', 'group.write', 'group.admin']
96 96 has_perm = HasRepoGroupPermissionAny(*perm_set)(repo_group.group_name, 'FileStore check')
97 97 if not has_perm:
98 98 log.warning('Access to file store object `%s` forbidden', file_uid)
99 99 raise HTTPNotFound()
100 100
101 101 FileStore.bump_access_counter(file_uid)
102 102
103 103 file_path = self.storage.store_path(file_uid)
104 104 content_type = 'application/octet-stream'
105 105 content_encoding = None
106 106
107 107 _content_type, _encoding = self._guess_type(file_path)
108 108 if _content_type:
109 109 content_type = _content_type
110 110
111 111 # For file store we don't submit any session data, this logic tells the
112 112 # Session lib to skip it
113 113 setattr(self.request, '_file_response', True)
114 114 response = FileResponse(
115 115 file_path, request=self.request,
116 116 content_type=content_type, content_encoding=content_encoding)
117 117
118 118 file_name = db_obj.file_display_name
119 119
120 120 response.headers["Content-Disposition"] = (
121 121 'attachment; filename="{}"'.format(str(file_name))
122 122 )
123 123 response.headers["X-RC-Artifact-Id"] = str(db_obj.file_store_id)
124 124 response.headers["X-RC-Artifact-Desc"] = str(db_obj.file_description)
125 125 response.headers["X-RC-Artifact-Sha256"] = str(db_obj.file_hash)
126 126 return response
127 127
128 128 @LoginRequired()
129 129 @NotAnonymous()
130 130 @CSRFRequired()
131 @view_config(route_name='upload_file', request_method='POST', renderer='json_ext')
132 131 def upload_file(self):
133 132 self.load_default_context()
134 133 file_obj = self.request.POST.get(self.upload_key)
135 134
136 135 if file_obj is None:
137 136 return {'store_fid': None,
138 137 'access_path': None,
139 138 'error': '{} data field is missing'.format(self.upload_key)}
140 139
141 140 if not hasattr(file_obj, 'filename'):
142 141 return {'store_fid': None,
143 142 'access_path': None,
144 143 'error': 'filename cannot be read from the data field'}
145 144
146 145 filename = file_obj.filename
147 146
148 147 metadata = {
149 148 'user_uploaded': {'username': self._rhodecode_user.username,
150 149 'user_id': self._rhodecode_user.user_id,
151 150 'ip': self._rhodecode_user.ip_addr}}
152 151 try:
153 152 store_uid, metadata = self.storage.save_file(
154 153 file_obj.file, filename, extra_metadata=metadata)
155 154 except FileNotAllowedException:
156 155 return {'store_fid': None,
157 156 'access_path': None,
158 157 'error': 'File {} is not allowed.'.format(filename)}
159 158
160 159 except FileOverSizeException:
161 160 return {'store_fid': None,
162 161 'access_path': None,
163 162 'error': 'File {} is exceeding allowed limit.'.format(filename)}
164 163
165 164 try:
166 165 entry = FileStore.create(
167 166 file_uid=store_uid, filename=metadata["filename"],
168 167 file_hash=metadata["sha256"], file_size=metadata["size"],
169 168 file_description=u'upload attachment',
170 169 check_acl=False, user_id=self._rhodecode_user.user_id
171 170 )
172 171 Session().add(entry)
173 172 Session().commit()
174 173 log.debug('Stored upload in DB as %s', entry)
175 174 except Exception:
176 175 log.exception('Failed to store file %s', filename)
177 176 return {'store_fid': None,
178 177 'access_path': None,
179 178 'error': 'File {} failed to store in DB.'.format(filename)}
180 179
181 180 return {'store_fid': store_uid,
182 181 'access_path': h.route_path('download_file', fid=store_uid)}
183 182
184 183 # ACL is checked by scopes, if no scope the file is accessible to all
185 @view_config(route_name='download_file')
186 184 def download_file(self):
187 185 self.load_default_context()
188 186 file_uid = self.request.matchdict['fid']
189 187 log.debug('Requesting FID:%s from store %s', file_uid, self.storage)
190 188 return self._serve_file(file_uid)
191 189
192 190 # in addition to @LoginRequired ACL is checked by scopes
193 191 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_ARTIFACT_DOWNLOAD])
194 192 @NotAnonymous()
195 @view_config(route_name='download_file_by_token')
196 193 def download_file_by_token(self):
197 194 """
198 195 Special view that allows to access the download file by special URL that
199 196 is stored inside the URL.
200 197
201 198 http://example.com/_file_store/token-download/TOKEN/FILE_UID
202 199 """
203 200 self.load_default_context()
204 201 file_uid = self.request.matchdict['fid']
205 202 return self._serve_file(file_uid)
@@ -1,62 +1,120 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
21 21
22 22
23 23 def admin_routes(config):
24 from rhodecode.apps.gist.views import GistView
25
24 26 config.add_route(
25 27 name='gists_show', pattern='/gists')
28 config.add_view(
29 GistView,
30 attr='gist_show_all',
31 route_name='gists_show', request_method='GET',
32 renderer='rhodecode:templates/admin/gists/gist_index.mako')
33
26 34 config.add_route(
27 35 name='gists_new', pattern='/gists/new')
36 config.add_view(
37 GistView,
38 attr='gist_new',
39 route_name='gists_new', request_method='GET',
40 renderer='rhodecode:templates/admin/gists/gist_new.mako')
41
28 42 config.add_route(
29 43 name='gists_create', pattern='/gists/create')
44 config.add_view(
45 GistView,
46 attr='gist_create',
47 route_name='gists_create', request_method='POST',
48 renderer='rhodecode:templates/admin/gists/gist_new.mako')
30 49
31 50 config.add_route(
32 51 name='gist_show', pattern='/gists/{gist_id}')
52 config.add_view(
53 GistView,
54 attr='gist_show',
55 route_name='gist_show', request_method='GET',
56 renderer='rhodecode:templates/admin/gists/gist_show.mako')
57
58 config.add_route(
59 name='gist_show_rev',
60 pattern='/gists/{gist_id}/rev/{revision}')
61
62 config.add_view(
63 GistView,
64 attr='gist_show',
65 route_name='gist_show_rev', request_method='GET',
66 renderer='rhodecode:templates/admin/gists/gist_show.mako')
67
68 config.add_route(
69 name='gist_show_formatted',
70 pattern='/gists/{gist_id}/rev/{revision}/{format}')
71 config.add_view(
72 GistView,
73 attr='gist_show',
74 route_name='gist_show_formatted', request_method='GET',
75 renderer=None)
76
77 config.add_route(
78 name='gist_show_formatted_path',
79 pattern='/gists/{gist_id}/rev/{revision}/{format}/{f_path:.*}')
80 config.add_view(
81 GistView,
82 attr='gist_show',
83 route_name='gist_show_formatted_path', request_method='GET',
84 renderer=None)
33 85
34 86 config.add_route(
35 87 name='gist_delete', pattern='/gists/{gist_id}/delete')
88 config.add_view(
89 GistView,
90 attr='gist_delete',
91 route_name='gist_delete', request_method='POST')
36 92
37 93 config.add_route(
38 94 name='gist_edit', pattern='/gists/{gist_id}/edit')
95 config.add_view(
96 GistView,
97 attr='gist_edit',
98 route_name='gist_edit', request_method='GET',
99 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
100
101 config.add_route(
102 name='gist_update', pattern='/gists/{gist_id}/update')
103 config.add_view(
104 GistView,
105 attr='gist_update',
106 route_name='gist_update', request_method='POST',
107 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
39 108
40 109 config.add_route(
41 110 name='gist_edit_check_revision',
42 111 pattern='/gists/{gist_id}/edit/check_revision')
43
44 config.add_route(
45 name='gist_update', pattern='/gists/{gist_id}/update')
46
47 config.add_route(
48 name='gist_show_rev',
49 pattern='/gists/{gist_id}/{revision}')
50 config.add_route(
51 name='gist_show_formatted',
52 pattern='/gists/{gist_id}/{revision}/{format}')
53
54 config.add_route(
55 name='gist_show_formatted_path',
56 pattern='/gists/{gist_id}/{revision}/{format}/{f_path:.*}')
112 config.add_view(
113 GistView,
114 attr='gist_edit_check_revision',
115 route_name='gist_edit_check_revision', request_method='GET',
116 renderer='json_ext')
57 117
58 118
59 119 def includeme(config):
60 120 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
61 # Scan module for configuration decorators.
62 config.scan('.views', ignore='.tests')
@@ -1,391 +1,391 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 mock
22 22 import pytest
23 23
24 24 from rhodecode.lib import helpers as h
25 25 from rhodecode.model.db import User, Gist
26 26 from rhodecode.model.gist import GistModel
27 27 from rhodecode.model.meta import Session
28 28 from rhodecode.tests import (
29 29 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
30 30 TestController, assert_session_flash)
31 31
32 32
33 33 def route_path(name, params=None, **kwargs):
34 34 import urllib
35 35 from rhodecode.apps._base import ADMIN_PREFIX
36 36
37 37 base_url = {
38 38 'gists_show': ADMIN_PREFIX + '/gists',
39 39 'gists_new': ADMIN_PREFIX + '/gists/new',
40 40 'gists_create': ADMIN_PREFIX + '/gists/create',
41 41 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
42 42 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
43 43 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
44 44 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
45 45 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/{revision}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}/{f_path}',
46 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}',
47 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}',
48 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/rev/{revision}/{format}/{f_path}',
49 49
50 50 }[name].format(**kwargs)
51 51
52 52 if params:
53 53 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
54 54 return base_url
55 55
56 56
57 57 class GistUtility(object):
58 58
59 59 def __init__(self):
60 60 self._gist_ids = []
61 61
62 62 def __call__(
63 63 self, f_name, content='some gist', lifetime=-1,
64 64 description='gist-desc', gist_type='public',
65 65 acl_level=Gist.GIST_PUBLIC, owner=TEST_USER_ADMIN_LOGIN):
66 66 gist_mapping = {
67 67 f_name: {'content': content}
68 68 }
69 69 user = User.get_by_username(owner)
70 70 gist = GistModel().create(
71 71 description, owner=user, gist_mapping=gist_mapping,
72 72 gist_type=gist_type, lifetime=lifetime, gist_acl_level=acl_level)
73 73 Session().commit()
74 74 self._gist_ids.append(gist.gist_id)
75 75 return gist
76 76
77 77 def cleanup(self):
78 78 for gist_id in self._gist_ids:
79 79 gist = Gist.get(gist_id)
80 80 if gist:
81 81 Session().delete(gist)
82 82
83 83 Session().commit()
84 84
85 85
86 86 @pytest.fixture()
87 87 def create_gist(request):
88 88 gist_utility = GistUtility()
89 89 request.addfinalizer(gist_utility.cleanup)
90 90 return gist_utility
91 91
92 92
93 93 class TestGistsController(TestController):
94 94
95 95 def test_index_empty(self, create_gist):
96 96 self.log_user()
97 97 response = self.app.get(route_path('gists_show'))
98 98 response.mustcontain('data: [],')
99 99
100 100 def test_index(self, create_gist):
101 101 self.log_user()
102 102 g1 = create_gist('gist1')
103 103 g2 = create_gist('gist2', lifetime=1400)
104 104 g3 = create_gist('gist3', description='gist3-desc')
105 105 g4 = create_gist('gist4', gist_type='private').gist_access_id
106 106 response = self.app.get(route_path('gists_show'))
107 107
108 108 response.mustcontain(g1.gist_access_id)
109 109 response.mustcontain(g2.gist_access_id)
110 110 response.mustcontain(g3.gist_access_id)
111 111 response.mustcontain('gist3-desc')
112 112 response.mustcontain(no=[g4])
113 113
114 114 # Expiration information should be visible
115 115 expires_tag = '%s' % h.age_component(
116 116 h.time_to_utcdatetime(g2.gist_expires))
117 117 response.mustcontain(expires_tag.replace('"', '\\"'))
118 118
119 119 def test_index_private_gists(self, create_gist):
120 120 self.log_user()
121 121 gist = create_gist('gist5', gist_type='private')
122 122 response = self.app.get(route_path('gists_show', params=dict(private=1)))
123 123
124 124 # and privates
125 125 response.mustcontain(gist.gist_access_id)
126 126
127 127 def test_index_show_all(self, create_gist):
128 128 self.log_user()
129 129 create_gist('gist1')
130 130 create_gist('gist2', lifetime=1400)
131 131 create_gist('gist3', description='gist3-desc')
132 132 create_gist('gist4', gist_type='private')
133 133
134 134 response = self.app.get(route_path('gists_show', params=dict(all=1)))
135 135
136 136 assert len(GistModel.get_all()) == 4
137 137 # and privates
138 138 for gist in GistModel.get_all():
139 139 response.mustcontain(gist.gist_access_id)
140 140
141 141 def test_index_show_all_hidden_from_regular(self, create_gist):
142 142 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
143 143 create_gist('gist2', gist_type='private')
144 144 create_gist('gist3', gist_type='private')
145 145 create_gist('gist4', gist_type='private')
146 146
147 147 response = self.app.get(route_path('gists_show', params=dict(all=1)))
148 148
149 149 assert len(GistModel.get_all()) == 3
150 150 # since we don't have access to private in this view, we
151 151 # should see nothing
152 152 for gist in GistModel.get_all():
153 153 response.mustcontain(no=[gist.gist_access_id])
154 154
155 155 def test_create(self):
156 156 self.log_user()
157 157 response = self.app.post(
158 158 route_path('gists_create'),
159 159 params={'lifetime': -1,
160 160 'content': 'gist test',
161 161 'filename': 'foo',
162 162 'gist_type': 'public',
163 163 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
164 164 'csrf_token': self.csrf_token},
165 165 status=302)
166 166 response = response.follow()
167 167 response.mustcontain('added file: foo')
168 168 response.mustcontain('gist test')
169 169
170 170 def test_create_with_path_with_dirs(self):
171 171 self.log_user()
172 172 response = self.app.post(
173 173 route_path('gists_create'),
174 174 params={'lifetime': -1,
175 175 'content': 'gist test',
176 176 'filename': '/home/foo',
177 177 'gist_type': 'public',
178 178 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
179 179 'csrf_token': self.csrf_token},
180 180 status=200)
181 181 response.mustcontain('Filename /home/foo cannot be inside a directory')
182 182
183 183 def test_access_expired_gist(self, create_gist):
184 184 self.log_user()
185 185 gist = create_gist('never-see-me')
186 186 gist.gist_expires = 0 # 1970
187 187 Session().add(gist)
188 188 Session().commit()
189 189
190 190 self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
191 191 status=404)
192 192
193 193 def test_create_private(self):
194 194 self.log_user()
195 195 response = self.app.post(
196 196 route_path('gists_create'),
197 197 params={'lifetime': -1,
198 198 'content': 'private gist test',
199 199 'filename': 'private-foo',
200 200 'gist_type': 'private',
201 201 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
202 202 'csrf_token': self.csrf_token},
203 203 status=302)
204 204 response = response.follow()
205 205 response.mustcontain('added file: private-foo<')
206 206 response.mustcontain('private gist test')
207 207 response.mustcontain('Private Gist')
208 208 # Make sure private gists are not indexed by robots
209 209 response.mustcontain(
210 210 '<meta name="robots" content="noindex, nofollow">')
211 211
212 212 def test_create_private_acl_private(self):
213 213 self.log_user()
214 214 response = self.app.post(
215 215 route_path('gists_create'),
216 216 params={'lifetime': -1,
217 217 'content': 'private gist test',
218 218 'filename': 'private-foo',
219 219 'gist_type': 'private',
220 220 'gist_acl_level': Gist.ACL_LEVEL_PRIVATE,
221 221 'csrf_token': self.csrf_token},
222 222 status=302)
223 223 response = response.follow()
224 224 response.mustcontain('added file: private-foo<')
225 225 response.mustcontain('private gist test')
226 226 response.mustcontain('Private Gist')
227 227 # Make sure private gists are not indexed by robots
228 228 response.mustcontain(
229 229 '<meta name="robots" content="noindex, nofollow">')
230 230
231 231 def test_create_with_description(self):
232 232 self.log_user()
233 233 response = self.app.post(
234 234 route_path('gists_create'),
235 235 params={'lifetime': -1,
236 236 'content': 'gist test',
237 237 'filename': 'foo-desc',
238 238 'description': 'gist-desc',
239 239 'gist_type': 'public',
240 240 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
241 241 'csrf_token': self.csrf_token},
242 242 status=302)
243 243 response = response.follow()
244 244 response.mustcontain('added file: foo-desc')
245 245 response.mustcontain('gist test')
246 246 response.mustcontain('gist-desc')
247 247
248 248 def test_create_public_with_anonymous_access(self):
249 249 self.log_user()
250 250 params = {
251 251 'lifetime': -1,
252 252 'content': 'gist test',
253 253 'filename': 'foo-desc',
254 254 'description': 'gist-desc',
255 255 'gist_type': 'public',
256 256 'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
257 257 'csrf_token': self.csrf_token
258 258 }
259 259 response = self.app.post(
260 260 route_path('gists_create'), params=params, status=302)
261 261 self.logout_user()
262 262 response = response.follow()
263 263 response.mustcontain('added file: foo-desc')
264 264 response.mustcontain('gist test')
265 265 response.mustcontain('gist-desc')
266 266
267 267 def test_new(self):
268 268 self.log_user()
269 269 self.app.get(route_path('gists_new'))
270 270
271 271 def test_delete(self, create_gist):
272 272 self.log_user()
273 273 gist = create_gist('delete-me')
274 274 response = self.app.post(
275 275 route_path('gist_delete', gist_id=gist.gist_id),
276 276 params={'csrf_token': self.csrf_token})
277 277 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
278 278
279 279 def test_delete_normal_user_his_gist(self, create_gist):
280 280 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
281 281 gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
282 282
283 283 response = self.app.post(
284 284 route_path('gist_delete', gist_id=gist.gist_id),
285 285 params={'csrf_token': self.csrf_token})
286 286 assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
287 287
288 288 def test_delete_normal_user_not_his_own_gist(self, create_gist):
289 289 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
290 290 gist = create_gist('delete-me-2')
291 291
292 292 self.app.post(
293 293 route_path('gist_delete', gist_id=gist.gist_id),
294 294 params={'csrf_token': self.csrf_token}, status=404)
295 295
296 296 def test_show(self, create_gist):
297 297 gist = create_gist('gist-show-me')
298 298 response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
299 299
300 300 response.mustcontain('added file: gist-show-me<')
301 301
302 302 assert_response = response.assert_response()
303 303 assert_response.element_equals_to(
304 304 'div.rc-user span.user',
305 305 '<a href="/_profiles/test_admin">test_admin</a>')
306 306
307 307 response.mustcontain('gist-desc')
308 308
309 309 def test_show_without_hg(self, create_gist):
310 310 with mock.patch(
311 311 'rhodecode.lib.vcs.settings.ALIASES', ['git']):
312 312 gist = create_gist('gist-show-me-again')
313 313 self.app.get(
314 314 route_path('gist_show', gist_id=gist.gist_access_id), status=200)
315 315
316 316 def test_show_acl_private(self, create_gist):
317 317 gist = create_gist('gist-show-me-only-when-im-logged-in',
318 318 acl_level=Gist.ACL_LEVEL_PRIVATE)
319 319 self.app.get(
320 320 route_path('gist_show', gist_id=gist.gist_access_id), status=404)
321 321
322 322 # now we log-in we should see thi gist
323 323 self.log_user()
324 324 response = self.app.get(
325 325 route_path('gist_show', gist_id=gist.gist_access_id))
326 326 response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
327 327
328 328 assert_response = response.assert_response()
329 329 assert_response.element_equals_to(
330 330 'div.rc-user span.user',
331 331 '<a href="/_profiles/test_admin">test_admin</a>')
332 332 response.mustcontain('gist-desc')
333 333
334 334 def test_show_as_raw(self, create_gist):
335 335 gist = create_gist('gist-show-me', content='GIST CONTENT')
336 336 response = self.app.get(
337 337 route_path('gist_show_formatted',
338 338 gist_id=gist.gist_access_id, revision='tip',
339 339 format='raw'))
340 340 assert response.body == 'GIST CONTENT'
341 341
342 342 def test_show_as_raw_individual_file(self, create_gist):
343 343 gist = create_gist('gist-show-me-raw', content='GIST BODY')
344 344 response = self.app.get(
345 345 route_path('gist_show_formatted_path',
346 346 gist_id=gist.gist_access_id, format='raw',
347 347 revision='tip', f_path='gist-show-me-raw'))
348 348 assert response.body == 'GIST BODY'
349 349
350 350 def test_edit_page(self, create_gist):
351 351 self.log_user()
352 352 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
353 353 response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
354 354 response.mustcontain('GIST EDIT BODY')
355 355
356 356 def test_edit_page_non_logged_user(self, create_gist):
357 357 gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
358 358 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
359 359 status=302)
360 360
361 361 def test_edit_normal_user_his_gist(self, create_gist):
362 362 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
363 363 gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
364 364 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
365 365 status=200))
366 366
367 367 def test_edit_normal_user_not_his_own_gist(self, create_gist):
368 368 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
369 369 gist = create_gist('delete-me')
370 370 self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
371 371 status=404)
372 372
373 373 def test_user_first_name_is_escaped(self, user_util, create_gist):
374 374 xss_atack_string = '"><script>alert(\'First Name\')</script>'
375 375 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
376 376 password = 'test'
377 377 user = user_util.create_user(
378 378 firstname=xss_atack_string, password=password)
379 379 create_gist('gist', gist_type='public', owner=user.username)
380 380 response = self.app.get(route_path('gists_show'))
381 381 response.mustcontain(xss_escaped_string)
382 382
383 383 def test_user_last_name_is_escaped(self, user_util, create_gist):
384 384 xss_atack_string = '"><script>alert(\'Last Name\')</script>'
385 385 xss_escaped_string = h.html_escape(h.escape(xss_atack_string))
386 386 password = 'test'
387 387 user = user_util.create_user(
388 388 lastname=xss_atack_string, password=password)
389 389 create_gist('gist', gist_type='public', owner=user.username)
390 390 response = self.app.get(route_path('gists_show'))
391 391 response.mustcontain(xss_escaped_string)
@@ -1,419 +1,386 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2020 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 import logging
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
29 from pyramid.view import view_config
30 29 from pyramid.renderers import render
31 30 from pyramid.response import Response
32 31
33 32 from rhodecode.apps._base import BaseAppView
34 33 from rhodecode.lib import helpers as h
35 34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 35 from rhodecode.lib.utils2 import time_to_datetime
37 36 from rhodecode.lib.ext_json import json
38 37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
39 38 from rhodecode.model.gist import GistModel
40 39 from rhodecode.model.meta import Session
41 40 from rhodecode.model.db import Gist, User, or_
42 41 from rhodecode.model import validation_schema
43 42 from rhodecode.model.validation_schema.schemas import gist_schema
44 43
45 44
46 45 log = logging.getLogger(__name__)
47 46
48 47
49 48 class GistView(BaseAppView):
50 49
51 50 def load_default_context(self):
52 51 _ = self.request.translate
53 52 c = self._get_local_tmpl_context()
54 53 c.user = c.auth_user.get_instance()
55 54
56 55 c.lifetime_values = [
57 56 (-1, _('forever')),
58 57 (5, _('5 minutes')),
59 58 (60, _('1 hour')),
60 59 (60 * 24, _('1 day')),
61 60 (60 * 24 * 30, _('1 month')),
62 61 ]
63 62
64 63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
65 64 c.acl_options = [
66 65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
67 66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
68 67 ]
69 68
70 69 return c
71 70
72 71 @LoginRequired()
73 @view_config(
74 route_name='gists_show', request_method='GET',
75 renderer='rhodecode:templates/admin/gists/gist_index.mako')
76 72 def gist_show_all(self):
77 73 c = self.load_default_context()
78 74
79 75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
80 76 c.show_private = self.request.GET.get('private') and not_default_user
81 77 c.show_public = self.request.GET.get('public') and not_default_user
82 78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
83 79
84 80 gists = _gists = Gist().query()\
85 81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
86 82 .order_by(Gist.created_on.desc())
87 83
88 84 c.active = 'public'
89 85 # MY private
90 86 if c.show_private and not c.show_public:
91 87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
92 88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 89 c.active = 'my_private'
94 90 # MY public
95 91 elif c.show_public and not c.show_private:
96 92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
97 93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
98 94 c.active = 'my_public'
99 95 # MY public+private
100 96 elif c.show_private and c.show_public:
101 97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
102 98 Gist.gist_type == Gist.GIST_PRIVATE))\
103 99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
104 100 c.active = 'my_all'
105 101 # Show all by super-admin
106 102 elif c.show_all:
107 103 c.active = 'all'
108 104 gists = _gists
109 105
110 106 # default show ALL public gists
111 107 if not c.show_public and not c.show_private and not c.show_all:
112 108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
113 109 c.active = 'public'
114 110
115 111 _render = self.request.get_partial_renderer(
116 112 'rhodecode:templates/data_table/_dt_elements.mako')
117 113
118 114 data = []
119 115
120 116 for gist in gists:
121 117 data.append({
122 118 'created_on': _render('gist_created', gist.created_on),
123 119 'created_on_raw': gist.created_on,
124 120 'type': _render('gist_type', gist.gist_type),
125 121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
126 122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
127 123 'author_raw': h.escape(gist.owner.full_contact),
128 124 'expires': _render('gist_expires', gist.gist_expires),
129 125 'description': _render('gist_description', gist.gist_description)
130 126 })
131 127 c.data = json.dumps(data)
132 128
133 129 return self._get_template_context(c)
134 130
135 131 @LoginRequired()
136 132 @NotAnonymous()
137 @view_config(
138 route_name='gists_new', request_method='GET',
139 renderer='rhodecode:templates/admin/gists/gist_new.mako')
140 133 def gist_new(self):
141 134 c = self.load_default_context()
142 135 return self._get_template_context(c)
143 136
144 137 @LoginRequired()
145 138 @NotAnonymous()
146 139 @CSRFRequired()
147 @view_config(
148 route_name='gists_create', request_method='POST',
149 renderer='rhodecode:templates/admin/gists/gist_new.mako')
150 140 def gist_create(self):
151 141 _ = self.request.translate
152 142 c = self.load_default_context()
153 143
154 144 data = dict(self.request.POST)
155 145 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
156 146
157 147 data['nodes'] = [{
158 148 'filename': data['filename'],
159 149 'content': data.get('content'),
160 150 'mimetype': data.get('mimetype') # None is autodetect
161 151 }]
162 152 gist_type = {
163 153 'public': Gist.GIST_PUBLIC,
164 154 'private': Gist.GIST_PRIVATE
165 155 }.get(data.get('gist_type')) or Gist.GIST_PRIVATE
166 156
167 157 data['gist_type'] = gist_type
168 158
169 159 data['gist_acl_level'] = (
170 160 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
171 161
172 162 schema = gist_schema.GistSchema().bind(
173 163 lifetime_options=[x[0] for x in c.lifetime_values])
174 164
175 165 try:
176 166
177 167 schema_data = schema.deserialize(data)
178 168 # convert to safer format with just KEYs so we sure no duplicates
179 169 schema_data['nodes'] = gist_schema.sequence_to_nodes(
180 170 schema_data['nodes'])
181 171
182 172 gist = GistModel().create(
183 173 gist_id=schema_data['gistid'], # custom access id not real ID
184 174 description=schema_data['description'],
185 175 owner=self._rhodecode_user.user_id,
186 176 gist_mapping=schema_data['nodes'],
187 177 gist_type=schema_data['gist_type'],
188 178 lifetime=schema_data['lifetime'],
189 179 gist_acl_level=schema_data['gist_acl_level']
190 180 )
191 181 Session().commit()
192 182 new_gist_id = gist.gist_access_id
193 183 except validation_schema.Invalid as errors:
194 184 defaults = data
195 185 errors = errors.asdict()
196 186
197 187 if 'nodes.0.content' in errors:
198 188 errors['content'] = errors['nodes.0.content']
199 189 del errors['nodes.0.content']
200 190 if 'nodes.0.filename' in errors:
201 191 errors['filename'] = errors['nodes.0.filename']
202 192 del errors['nodes.0.filename']
203 193
204 194 data = render('rhodecode:templates/admin/gists/gist_new.mako',
205 195 self._get_template_context(c), self.request)
206 196 html = formencode.htmlfill.render(
207 197 data,
208 198 defaults=defaults,
209 199 errors=errors,
210 200 prefix_error=False,
211 201 encoding="UTF-8",
212 202 force_defaults=False
213 203 )
214 204 return Response(html)
215 205
216 206 except Exception:
217 207 log.exception("Exception while trying to create a gist")
218 208 h.flash(_('Error occurred during gist creation'), category='error')
219 209 raise HTTPFound(h.route_url('gists_new'))
220 210 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
221 211
222 212 @LoginRequired()
223 213 @NotAnonymous()
224 214 @CSRFRequired()
225 @view_config(
226 route_name='gist_delete', request_method='POST')
227 215 def gist_delete(self):
228 216 _ = self.request.translate
229 217 gist_id = self.request.matchdict['gist_id']
230 218
231 219 c = self.load_default_context()
232 220 c.gist = Gist.get_or_404(gist_id)
233 221
234 222 owner = c.gist.gist_owner == self._rhodecode_user.user_id
235 223 if not (h.HasPermissionAny('hg.admin')() or owner):
236 224 log.warning('Deletion of Gist was forbidden '
237 225 'by unauthorized user: `%s`', self._rhodecode_user)
238 226 raise HTTPNotFound()
239 227
240 228 GistModel().delete(c.gist)
241 229 Session().commit()
242 230 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
243 231
244 232 raise HTTPFound(h.route_url('gists_show'))
245 233
246 234 def _get_gist(self, gist_id):
247 235
248 236 gist = Gist.get_or_404(gist_id)
249 237
250 238 # Check if this gist is expired
251 239 if gist.gist_expires != -1:
252 240 if time.time() > gist.gist_expires:
253 241 log.error(
254 242 'Gist expired at %s', time_to_datetime(gist.gist_expires))
255 243 raise HTTPNotFound()
256 244
257 245 # check if this gist requires a login
258 246 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
259 247 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
260 248 log.error("Anonymous user %s tried to access protected gist `%s`",
261 249 self._rhodecode_user, gist_id)
262 250 raise HTTPNotFound()
263 251 return gist
264 252
265 253 @LoginRequired()
266 @view_config(
267 route_name='gist_show', request_method='GET',
268 renderer='rhodecode:templates/admin/gists/gist_show.mako')
269 @view_config(
270 route_name='gist_show_rev', request_method='GET',
271 renderer='rhodecode:templates/admin/gists/gist_show.mako')
272 @view_config(
273 route_name='gist_show_formatted', request_method='GET',
274 renderer=None)
275 @view_config(
276 route_name='gist_show_formatted_path', request_method='GET',
277 renderer=None)
278 254 def gist_show(self):
279 255 gist_id = self.request.matchdict['gist_id']
280 256
281 257 # TODO(marcink): expose those via matching dict
282 258 revision = self.request.matchdict.get('revision', 'tip')
283 259 f_path = self.request.matchdict.get('f_path', None)
284 260 return_format = self.request.matchdict.get('format')
285 261
286 262 c = self.load_default_context()
287 263 c.gist = self._get_gist(gist_id)
288 264 c.render = not self.request.GET.get('no-render', False)
289 265
290 266 try:
291 267 c.file_last_commit, c.files = GistModel().get_gist_files(
292 268 gist_id, revision=revision)
293 269 except VCSError:
294 270 log.exception("Exception in gist show")
295 271 raise HTTPNotFound()
296 272
297 273 if return_format == 'raw':
298 274 content = '\n\n'.join([f.content for f in c.files
299 275 if (f_path is None or f.path == f_path)])
300 276 response = Response(content)
301 277 response.content_type = 'text/plain'
302 278 return response
303 279 elif return_format:
304 280 raise HTTPBadRequest()
305 281
306 282 return self._get_template_context(c)
307 283
308 284 @LoginRequired()
309 285 @NotAnonymous()
310 @view_config(
311 route_name='gist_edit', request_method='GET',
312 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
313 286 def gist_edit(self):
314 287 _ = self.request.translate
315 288 gist_id = self.request.matchdict['gist_id']
316 289 c = self.load_default_context()
317 290 c.gist = self._get_gist(gist_id)
318 291
319 292 owner = c.gist.gist_owner == self._rhodecode_user.user_id
320 293 if not (h.HasPermissionAny('hg.admin')() or owner):
321 294 raise HTTPNotFound()
322 295
323 296 try:
324 297 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
325 298 except VCSError:
326 299 log.exception("Exception in gist edit")
327 300 raise HTTPNotFound()
328 301
329 302 if c.gist.gist_expires == -1:
330 303 expiry = _('never')
331 304 else:
332 305 # this cannot use timeago, since it's used in select2 as a value
333 306 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
334 307
335 308 c.lifetime_values.append(
336 309 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
337 310 )
338 311
339 312 return self._get_template_context(c)
340 313
341 314 @LoginRequired()
342 315 @NotAnonymous()
343 316 @CSRFRequired()
344 @view_config(
345 route_name='gist_update', request_method='POST',
346 renderer='rhodecode:templates/admin/gists/gist_edit.mako')
347 317 def gist_update(self):
348 318 _ = self.request.translate
349 319 gist_id = self.request.matchdict['gist_id']
350 320 c = self.load_default_context()
351 321 c.gist = self._get_gist(gist_id)
352 322
353 323 owner = c.gist.gist_owner == self._rhodecode_user.user_id
354 324 if not (h.HasPermissionAny('hg.admin')() or owner):
355 325 raise HTTPNotFound()
356 326
357 327 data = peppercorn.parse(self.request.POST.items())
358 328
359 329 schema = gist_schema.GistSchema()
360 330 schema = schema.bind(
361 331 # '0' is special value to leave lifetime untouched
362 332 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
363 333 )
364 334
365 335 try:
366 336 schema_data = schema.deserialize(data)
367 337 # convert to safer format with just KEYs so we sure no duplicates
368 338 schema_data['nodes'] = gist_schema.sequence_to_nodes(
369 339 schema_data['nodes'])
370 340
371 341 GistModel().update(
372 342 gist=c.gist,
373 343 description=schema_data['description'],
374 344 owner=c.gist.owner,
375 345 gist_mapping=schema_data['nodes'],
376 346 lifetime=schema_data['lifetime'],
377 347 gist_acl_level=schema_data['gist_acl_level']
378 348 )
379 349
380 350 Session().commit()
381 351 h.flash(_('Successfully updated gist content'), category='success')
382 352 except NodeNotChangedError:
383 353 # raised if nothing was changed in repo itself. We anyway then
384 354 # store only DB stuff for gist
385 355 Session().commit()
386 356 h.flash(_('Successfully updated gist data'), category='success')
387 357 except validation_schema.Invalid as errors:
388 358 errors = h.escape(errors.asdict())
389 359 h.flash(_('Error occurred during update of gist {}: {}').format(
390 360 gist_id, errors), category='error')
391 361 except Exception:
392 362 log.exception("Exception in gist edit")
393 363 h.flash(_('Error occurred during update of gist %s') % gist_id,
394 364 category='error')
395 365
396 366 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
397 367
398 368 @LoginRequired()
399 369 @NotAnonymous()
400 @view_config(
401 route_name='gist_edit_check_revision', request_method='GET',
402 renderer='json_ext')
403 370 def gist_edit_check_revision(self):
404 371 _ = self.request.translate
405 372 gist_id = self.request.matchdict['gist_id']
406 373 c = self.load_default_context()
407 374 c.gist = self._get_gist(gist_id)
408 375
409 376 last_rev = c.gist.scm_instance().get_commit()
410 377 success = True
411 378 revision = self.request.GET.get('revision')
412 379
413 380 if revision != last_rev.raw_id:
414 381 log.error('Last revision %s is different then submitted %s',
415 382 revision, last_rev)
416 383 # our gist has newer version than we
417 384 success = False
418 385
419 386 return {'success': success}
@@ -1,93 +1,147 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.config import routing_links
21 21
22 22
23 23 class VCSCallPredicate(object):
24 24 def __init__(self, val, config):
25 25 self.val = val
26 26
27 27 def text(self):
28 28 return 'vcs_call route = %s' % self.val
29 29
30 30 phash = text
31 31
32 32 def __call__(self, info, request):
33 33 if hasattr(request, 'vcs_call'):
34 34 # skip vcs calls
35 35 return False
36 36
37 37 return True
38 38
39 39
40 40 def includeme(config):
41 from rhodecode.apps.home.views import HomeView
42
43 config.add_route_predicate(
44 'skip_vcs_call', VCSCallPredicate)
41 45
42 46 config.add_route(
43 47 name='home',
44 48 pattern='/')
49 config.add_view(
50 HomeView,
51 attr='main_page',
52 route_name='home', request_method='GET',
53 renderer='rhodecode:templates/index.mako')
45 54
46 55 config.add_route(
47 56 name='main_page_repos_data',
48 57 pattern='/_home_repos')
58 config.add_view(
59 HomeView,
60 attr='main_page_repos_data',
61 route_name='main_page_repos_data',
62 request_method='GET', renderer='json_ext', xhr=True)
49 63
50 64 config.add_route(
51 65 name='main_page_repo_groups_data',
52 66 pattern='/_home_repo_groups')
67 config.add_view(
68 HomeView,
69 attr='main_page_repo_groups_data',
70 route_name='main_page_repo_groups_data',
71 request_method='GET', renderer='json_ext', xhr=True)
53 72
54 73 config.add_route(
55 74 name='user_autocomplete_data',
56 75 pattern='/_users')
76 config.add_view(
77 HomeView,
78 attr='user_autocomplete_data',
79 route_name='user_autocomplete_data', request_method='GET',
80 renderer='json_ext', xhr=True)
57 81
58 82 config.add_route(
59 83 name='user_group_autocomplete_data',
60 84 pattern='/_user_groups')
85 config.add_view(
86 HomeView,
87 attr='user_group_autocomplete_data',
88 route_name='user_group_autocomplete_data', request_method='GET',
89 renderer='json_ext', xhr=True)
61 90
62 91 config.add_route(
63 92 name='repo_list_data',
64 93 pattern='/_repos')
94 config.add_view(
95 HomeView,
96 attr='repo_list_data',
97 route_name='repo_list_data', request_method='GET',
98 renderer='json_ext', xhr=True)
65 99
66 100 config.add_route(
67 101 name='repo_group_list_data',
68 102 pattern='/_repo_groups')
103 config.add_view(
104 HomeView,
105 attr='repo_group_list_data',
106 route_name='repo_group_list_data', request_method='GET',
107 renderer='json_ext', xhr=True)
69 108
70 109 config.add_route(
71 110 name='goto_switcher_data',
72 111 pattern='/_goto_data')
112 config.add_view(
113 HomeView,
114 attr='goto_switcher_data',
115 route_name='goto_switcher_data', request_method='GET',
116 renderer='json_ext', xhr=True)
73 117
74 118 config.add_route(
75 119 name='markup_preview',
76 120 pattern='/_markup_preview')
121 config.add_view(
122 HomeView,
123 attr='markup_preview',
124 route_name='markup_preview', request_method='POST',
125 renderer='string', xhr=True)
77 126
78 127 config.add_route(
79 128 name='file_preview',
80 129 pattern='/_file_preview')
130 config.add_view(
131 HomeView,
132 attr='file_preview',
133 route_name='file_preview', request_method='POST',
134 renderer='string', xhr=True)
81 135
82 136 config.add_route(
83 137 name='store_user_session_value',
84 138 pattern='/_store_session_attr')
139 config.add_view(
140 HomeView,
141 attr='store_user_session_attr',
142 route_name='store_user_session_value', request_method='POST',
143 renderer='string', xhr=True)
85 144
86 145 # register our static links via redirection mechanism
87 146 routing_links.connect_redirection_links(config)
88 147
89 # Scan module for configuration decorators.
90 config.scan('.views', ignore='.tests')
91
92 config.add_route_predicate(
93 'skip_vcs_call', VCSCallPredicate)
@@ -1,897 +1,856 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 from pyramid.view import view_config
27 26
28 27 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 28 from rhodecode.lib import helpers as h
30 29 from rhodecode.lib.auth import (
31 30 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 31 HasRepoGroupPermissionAny, AuthUser)
33 32 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 33 from rhodecode.lib.index import searcher_from_config
35 34 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int, safe_str
36 35 from rhodecode.lib.vcs.nodes import FileNode
37 36 from rhodecode.model.db import (
38 37 func, true, or_, case, cast, in_filter_generator, String, Session,
39 38 Repository, RepoGroup, User, UserGroup, PullRequest)
40 39 from rhodecode.model.repo import RepoModel
41 40 from rhodecode.model.repo_group import RepoGroupModel
42 41 from rhodecode.model.user import UserModel
43 42 from rhodecode.model.user_group import UserGroupModel
44 43
45 44 log = logging.getLogger(__name__)
46 45
47 46
48 47 class HomeView(BaseAppView, DataGridAppView):
49 48
50 49 def load_default_context(self):
51 50 c = self._get_local_tmpl_context()
52 51 c.user = c.auth_user.get_instance()
53
54 52 return c
55 53
56 54 @LoginRequired()
57 @view_config(
58 route_name='user_autocomplete_data', request_method='GET',
59 renderer='json_ext', xhr=True)
60 55 def user_autocomplete_data(self):
61 56 self.load_default_context()
62 57 query = self.request.GET.get('query')
63 58 active = str2bool(self.request.GET.get('active') or True)
64 59 include_groups = str2bool(self.request.GET.get('user_groups'))
65 60 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
66 61 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
67 62
68 63 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
69 64 query, active, include_groups)
70 65
71 66 _users = UserModel().get_users(
72 67 name_contains=query, only_active=active)
73 68
74 69 def maybe_skip_default_user(usr):
75 70 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
76 71 return False
77 72 return True
78 73 _users = filter(maybe_skip_default_user, _users)
79 74
80 75 if include_groups:
81 76 # extend with user groups
82 77 _user_groups = UserGroupModel().get_user_groups(
83 78 name_contains=query, only_active=active,
84 79 expand_groups=expand_groups)
85 80 _users = _users + _user_groups
86 81
87 82 return {'suggestions': _users}
88 83
89 84 @LoginRequired()
90 85 @NotAnonymous()
91 @view_config(
92 route_name='user_group_autocomplete_data', request_method='GET',
93 renderer='json_ext', xhr=True)
94 86 def user_group_autocomplete_data(self):
95 87 self.load_default_context()
96 88 query = self.request.GET.get('query')
97 89 active = str2bool(self.request.GET.get('active') or True)
98 90 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
99 91
100 92 log.debug('generating user group list, query:%s, active:%s',
101 93 query, active)
102 94
103 95 _user_groups = UserGroupModel().get_user_groups(
104 96 name_contains=query, only_active=active,
105 97 expand_groups=expand_groups)
106 98 _user_groups = _user_groups
107 99
108 100 return {'suggestions': _user_groups}
109 101
110 102 def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', limit=20):
111 103 org_query = name_contains
112 104 allowed_ids = self._rhodecode_user.repo_acl_ids(
113 105 ['repository.read', 'repository.write', 'repository.admin'],
114 106 cache=True, name_filter=name_contains) or [-1]
115 107
116 108 query = Session().query(
117 109 Repository.repo_name,
118 110 Repository.repo_id,
119 111 Repository.repo_type,
120 112 Repository.private,
121 113 )\
122 114 .filter(Repository.archived.isnot(true()))\
123 115 .filter(or_(
124 116 # generate multiple IN to fix limitation problems
125 117 *in_filter_generator(Repository.repo_id, allowed_ids)
126 118 ))
127 119
128 120 query = query.order_by(case(
129 121 [
130 122 (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'),
131 123 ],
132 124 ))
133 125 query = query.order_by(func.length(Repository.repo_name))
134 126 query = query.order_by(Repository.repo_name)
135 127
136 128 if repo_type:
137 129 query = query.filter(Repository.repo_type == repo_type)
138 130
139 131 if name_contains:
140 132 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
141 133 query = query.filter(
142 134 Repository.repo_name.ilike(ilike_expression))
143 135 query = query.limit(limit)
144 136
145 137 acl_iter = query
146 138
147 139 return [
148 140 {
149 141 'id': obj.repo_name,
150 142 'value': org_query,
151 143 'value_display': obj.repo_name,
152 144 'text': obj.repo_name,
153 145 'type': 'repo',
154 146 'repo_id': obj.repo_id,
155 147 'repo_type': obj.repo_type,
156 148 'private': obj.private,
157 149 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
158 150 }
159 151 for obj in acl_iter]
160 152
161 153 def _get_repo_group_list(self, name_contains=None, repo_group_name='', limit=20):
162 154 org_query = name_contains
163 155 allowed_ids = self._rhodecode_user.repo_group_acl_ids(
164 156 ['group.read', 'group.write', 'group.admin'],
165 157 cache=True, name_filter=name_contains) or [-1]
166 158
167 159 query = Session().query(
168 160 RepoGroup.group_id,
169 161 RepoGroup.group_name,
170 162 )\
171 163 .filter(or_(
172 164 # generate multiple IN to fix limitation problems
173 165 *in_filter_generator(RepoGroup.group_id, allowed_ids)
174 166 ))
175 167
176 168 query = query.order_by(case(
177 169 [
178 170 (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'),
179 171 ],
180 172 ))
181 173 query = query.order_by(func.length(RepoGroup.group_name))
182 174 query = query.order_by(RepoGroup.group_name)
183 175
184 176 if name_contains:
185 177 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
186 178 query = query.filter(
187 179 RepoGroup.group_name.ilike(ilike_expression))
188 180 query = query.limit(limit)
189 181
190 182 acl_iter = query
191 183
192 184 return [
193 185 {
194 186 'id': obj.group_name,
195 187 'value': org_query,
196 188 'value_display': obj.group_name,
197 189 'text': obj.group_name,
198 190 'type': 'repo_group',
199 191 'repo_group_id': obj.group_id,
200 192 'url': h.route_path(
201 193 'repo_group_home', repo_group_name=obj.group_name)
202 194 }
203 195 for obj in acl_iter]
204 196
205 197 def _get_user_list(self, name_contains=None, limit=20):
206 198 org_query = name_contains
207 199 if not name_contains:
208 200 return [], False
209 201
210 202 # TODO(marcink): should all logged in users be allowed to search others?
211 203 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
212 204 if not allowed_user_search:
213 205 return [], False
214 206
215 207 name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains)
216 208 if len(name_contains) != 1:
217 209 return [], False
218 210
219 211 name_contains = name_contains[0]
220 212
221 213 query = User.query()\
222 214 .order_by(func.length(User.username))\
223 215 .order_by(User.username) \
224 216 .filter(User.username != User.DEFAULT_USER)
225 217
226 218 if name_contains:
227 219 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
228 220 query = query.filter(
229 221 User.username.ilike(ilike_expression))
230 222 query = query.limit(limit)
231 223
232 224 acl_iter = query
233 225
234 226 return [
235 227 {
236 228 'id': obj.user_id,
237 229 'value': org_query,
238 230 'value_display': 'user: `{}`'.format(obj.username),
239 231 'type': 'user',
240 232 'icon_link': h.gravatar_url(obj.email, 30),
241 233 'url': h.route_path(
242 234 'user_profile', username=obj.username)
243 235 }
244 236 for obj in acl_iter], True
245 237
246 238 def _get_user_groups_list(self, name_contains=None, limit=20):
247 239 org_query = name_contains
248 240 if not name_contains:
249 241 return [], False
250 242
251 243 # TODO(marcink): should all logged in users be allowed to search others?
252 244 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
253 245 if not allowed_user_search:
254 246 return [], False
255 247
256 248 name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains)
257 249 if len(name_contains) != 1:
258 250 return [], False
259 251
260 252 name_contains = name_contains[0]
261 253
262 254 query = UserGroup.query()\
263 255 .order_by(func.length(UserGroup.users_group_name))\
264 256 .order_by(UserGroup.users_group_name)
265 257
266 258 if name_contains:
267 259 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
268 260 query = query.filter(
269 261 UserGroup.users_group_name.ilike(ilike_expression))
270 262 query = query.limit(limit)
271 263
272 264 acl_iter = query
273 265
274 266 return [
275 267 {
276 268 'id': obj.users_group_id,
277 269 'value': org_query,
278 270 'value_display': 'user_group: `{}`'.format(obj.users_group_name),
279 271 'type': 'user_group',
280 272 'url': h.route_path(
281 273 'user_group_profile', user_group_name=obj.users_group_name)
282 274 }
283 275 for obj in acl_iter], True
284 276
285 277 def _get_pull_request_list(self, name_contains=None, limit=20):
286 278 org_query = name_contains
287 279 if not name_contains:
288 280 return [], False
289 281
290 282 # TODO(marcink): should all logged in users be allowed to search others?
291 283 allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER
292 284 if not allowed_user_search:
293 285 return [], False
294 286
295 287 name_contains = re.compile('(?:pr:[ ]?)(.+)').findall(name_contains)
296 288 if len(name_contains) != 1:
297 289 return [], False
298 290
299 291 name_contains = name_contains[0]
300 292
301 293 allowed_ids = self._rhodecode_user.repo_acl_ids(
302 294 ['repository.read', 'repository.write', 'repository.admin'],
303 295 cache=True) or [-1]
304 296
305 297 query = Session().query(
306 298 PullRequest.pull_request_id,
307 299 PullRequest.title,
308 300 )
309 301 query = query.join(Repository, Repository.repo_id == PullRequest.target_repo_id)
310 302
311 303 query = query.filter(or_(
312 304 # generate multiple IN to fix limitation problems
313 305 *in_filter_generator(Repository.repo_id, allowed_ids)
314 306 ))
315 307
316 308 query = query.order_by(PullRequest.pull_request_id)
317 309
318 310 if name_contains:
319 311 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
320 312 query = query.filter(or_(
321 313 cast(PullRequest.pull_request_id, String).ilike(ilike_expression),
322 314 PullRequest.title.ilike(ilike_expression),
323 315 PullRequest.description.ilike(ilike_expression),
324 316 ))
325 317
326 318 query = query.limit(limit)
327 319
328 320 acl_iter = query
329 321
330 322 return [
331 323 {
332 324 'id': obj.pull_request_id,
333 325 'value': org_query,
334 326 'value_display': 'pull request: `!{} - {}`'.format(
335 327 obj.pull_request_id, safe_str(obj.title[:50])),
336 328 'type': 'pull_request',
337 329 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
338 330 }
339 331 for obj in acl_iter], True
340 332
341 333 def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None):
342 334 repo_name = repo_group_name = None
343 335 if repo:
344 336 repo_name = repo.repo_name
345 337 if repo_group:
346 338 repo_group_name = repo_group.group_name
347 339
348 340 org_query = query
349 341 if not query or len(query) < 3 or not searcher:
350 342 return [], False
351 343
352 344 commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query)
353 345
354 346 if len(commit_hashes) != 1:
355 347 return [], False
356 348
357 349 commit_hash = commit_hashes[0]
358 350
359 351 result = searcher.search(
360 352 'commit_id:{}*'.format(commit_hash), 'commit', auth_user,
361 353 repo_name, repo_group_name, raise_on_exc=False)
362 354
363 355 commits = []
364 356 for entry in result['results']:
365 357 repo_data = {
366 358 'repository_id': entry.get('repository_id'),
367 359 'repository_type': entry.get('repo_type'),
368 360 'repository_name': entry.get('repository'),
369 361 }
370 362
371 363 commit_entry = {
372 364 'id': entry['commit_id'],
373 365 'value': org_query,
374 366 'value_display': '`{}` commit: {}'.format(
375 367 entry['repository'], entry['commit_id']),
376 368 'type': 'commit',
377 369 'repo': entry['repository'],
378 370 'repo_data': repo_data,
379 371
380 372 'url': h.route_path(
381 373 'repo_commit',
382 374 repo_name=entry['repository'], commit_id=entry['commit_id'])
383 375 }
384 376
385 377 commits.append(commit_entry)
386 378 return commits, True
387 379
388 380 def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None):
389 381 repo_name = repo_group_name = None
390 382 if repo:
391 383 repo_name = repo.repo_name
392 384 if repo_group:
393 385 repo_group_name = repo_group.group_name
394 386
395 387 org_query = query
396 388 if not query or len(query) < 3 or not searcher:
397 389 return [], False
398 390
399 391 paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query)
400 392 if len(paths_re) != 1:
401 393 return [], False
402 394
403 395 file_path = paths_re[0]
404 396
405 397 search_path = searcher.escape_specials(file_path)
406 398 result = searcher.search(
407 399 'file.raw:*{}*'.format(search_path), 'path', auth_user,
408 400 repo_name, repo_group_name, raise_on_exc=False)
409 401
410 402 files = []
411 403 for entry in result['results']:
412 404 repo_data = {
413 405 'repository_id': entry.get('repository_id'),
414 406 'repository_type': entry.get('repo_type'),
415 407 'repository_name': entry.get('repository'),
416 408 }
417 409
418 410 file_entry = {
419 411 'id': entry['commit_id'],
420 412 'value': org_query,
421 413 'value_display': '`{}` file: {}'.format(
422 414 entry['repository'], entry['file']),
423 415 'type': 'file',
424 416 'repo': entry['repository'],
425 417 'repo_data': repo_data,
426 418
427 419 'url': h.route_path(
428 420 'repo_files',
429 421 repo_name=entry['repository'], commit_id=entry['commit_id'],
430 422 f_path=entry['file'])
431 423 }
432 424
433 425 files.append(file_entry)
434 426 return files, True
435 427
436 428 @LoginRequired()
437 @view_config(
438 route_name='repo_list_data', request_method='GET',
439 renderer='json_ext', xhr=True)
440 429 def repo_list_data(self):
441 430 _ = self.request.translate
442 431 self.load_default_context()
443 432
444 433 query = self.request.GET.get('query')
445 434 repo_type = self.request.GET.get('repo_type')
446 435 log.debug('generating repo list, query:%s, repo_type:%s',
447 436 query, repo_type)
448 437
449 438 res = []
450 439 repos = self._get_repo_list(query, repo_type=repo_type)
451 440 if repos:
452 441 res.append({
453 442 'text': _('Repositories'),
454 443 'children': repos
455 444 })
456 445
457 446 data = {
458 447 'more': False,
459 448 'results': res
460 449 }
461 450 return data
462 451
463 452 @LoginRequired()
464 @view_config(
465 route_name='repo_group_list_data', request_method='GET',
466 renderer='json_ext', xhr=True)
467 453 def repo_group_list_data(self):
468 454 _ = self.request.translate
469 455 self.load_default_context()
470 456
471 457 query = self.request.GET.get('query')
472 458
473 459 log.debug('generating repo group list, query:%s',
474 460 query)
475 461
476 462 res = []
477 463 repo_groups = self._get_repo_group_list(query)
478 464 if repo_groups:
479 465 res.append({
480 466 'text': _('Repository Groups'),
481 467 'children': repo_groups
482 468 })
483 469
484 470 data = {
485 471 'more': False,
486 472 'results': res
487 473 }
488 474 return data
489 475
490 476 def _get_default_search_queries(self, search_context, searcher, query):
491 477 if not searcher:
492 478 return []
493 479
494 480 is_es_6 = searcher.is_es_6
495 481
496 482 queries = []
497 483 repo_group_name, repo_name, repo_context = None, None, None
498 484
499 485 # repo group context
500 486 if search_context.get('search_context[repo_group_name]'):
501 487 repo_group_name = search_context.get('search_context[repo_group_name]')
502 488 if search_context.get('search_context[repo_name]'):
503 489 repo_name = search_context.get('search_context[repo_name]')
504 490 repo_context = search_context.get('search_context[repo_view_type]')
505 491
506 492 if is_es_6 and repo_name:
507 493 # files
508 494 def query_modifier():
509 495 qry = query
510 496 return {'q': qry, 'type': 'content'}
511 497
512 498 label = u'File content search for `{}`'.format(h.escape(query))
513 499 file_qry = {
514 500 'id': -10,
515 501 'value': query,
516 502 'value_display': label,
517 503 'value_icon': '<i class="icon-code"></i>',
518 504 'type': 'search',
519 505 'subtype': 'repo',
520 506 'url': h.route_path('search_repo',
521 507 repo_name=repo_name,
522 508 _query=query_modifier())
523 509 }
524 510
525 511 # commits
526 512 def query_modifier():
527 513 qry = query
528 514 return {'q': qry, 'type': 'commit'}
529 515
530 516 label = u'Commit search for `{}`'.format(h.escape(query))
531 517 commit_qry = {
532 518 'id': -20,
533 519 'value': query,
534 520 'value_display': label,
535 521 'value_icon': '<i class="icon-history"></i>',
536 522 'type': 'search',
537 523 'subtype': 'repo',
538 524 'url': h.route_path('search_repo',
539 525 repo_name=repo_name,
540 526 _query=query_modifier())
541 527 }
542 528
543 529 if repo_context in ['commit', 'commits']:
544 530 queries.extend([commit_qry, file_qry])
545 531 elif repo_context in ['files', 'summary']:
546 532 queries.extend([file_qry, commit_qry])
547 533 else:
548 534 queries.extend([commit_qry, file_qry])
549 535
550 536 elif is_es_6 and repo_group_name:
551 537 # files
552 538 def query_modifier():
553 539 qry = query
554 540 return {'q': qry, 'type': 'content'}
555 541
556 542 label = u'File content search for `{}`'.format(query)
557 543 file_qry = {
558 544 'id': -30,
559 545 'value': query,
560 546 'value_display': label,
561 547 'value_icon': '<i class="icon-code"></i>',
562 548 'type': 'search',
563 549 'subtype': 'repo_group',
564 550 'url': h.route_path('search_repo_group',
565 551 repo_group_name=repo_group_name,
566 552 _query=query_modifier())
567 553 }
568 554
569 555 # commits
570 556 def query_modifier():
571 557 qry = query
572 558 return {'q': qry, 'type': 'commit'}
573 559
574 560 label = u'Commit search for `{}`'.format(query)
575 561 commit_qry = {
576 562 'id': -40,
577 563 'value': query,
578 564 'value_display': label,
579 565 'value_icon': '<i class="icon-history"></i>',
580 566 'type': 'search',
581 567 'subtype': 'repo_group',
582 568 'url': h.route_path('search_repo_group',
583 569 repo_group_name=repo_group_name,
584 570 _query=query_modifier())
585 571 }
586 572
587 573 if repo_context in ['commit', 'commits']:
588 574 queries.extend([commit_qry, file_qry])
589 575 elif repo_context in ['files', 'summary']:
590 576 queries.extend([file_qry, commit_qry])
591 577 else:
592 578 queries.extend([commit_qry, file_qry])
593 579
594 580 # Global, not scoped
595 581 if not queries:
596 582 queries.append(
597 583 {
598 584 'id': -1,
599 585 'value': query,
600 586 'value_display': u'File content search for: `{}`'.format(query),
601 587 'value_icon': '<i class="icon-code"></i>',
602 588 'type': 'search',
603 589 'subtype': 'global',
604 590 'url': h.route_path('search',
605 591 _query={'q': query, 'type': 'content'})
606 592 })
607 593 queries.append(
608 594 {
609 595 'id': -2,
610 596 'value': query,
611 597 'value_display': u'Commit search for: `{}`'.format(query),
612 598 'value_icon': '<i class="icon-history"></i>',
613 599 'type': 'search',
614 600 'subtype': 'global',
615 601 'url': h.route_path('search',
616 602 _query={'q': query, 'type': 'commit'})
617 603 })
618 604
619 605 return queries
620 606
621 607 @LoginRequired()
622 @view_config(
623 route_name='goto_switcher_data', request_method='GET',
624 renderer='json_ext', xhr=True)
625 608 def goto_switcher_data(self):
626 609 c = self.load_default_context()
627 610
628 611 _ = self.request.translate
629 612
630 613 query = self.request.GET.get('query')
631 614 log.debug('generating main filter data, query %s', query)
632 615
633 616 res = []
634 617 if not query:
635 618 return {'suggestions': res}
636 619
637 620 def no_match(name):
638 621 return {
639 622 'id': -1,
640 623 'value': "",
641 624 'value_display': name,
642 625 'type': 'text',
643 626 'url': ""
644 627 }
645 628 searcher = searcher_from_config(self.request.registry.settings)
646 629 has_specialized_search = False
647 630
648 631 # set repo context
649 632 repo = None
650 633 repo_id = safe_int(self.request.GET.get('search_context[repo_id]'))
651 634 if repo_id:
652 635 repo = Repository.get(repo_id)
653 636
654 637 # set group context
655 638 repo_group = None
656 639 repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]'))
657 640 if repo_group_id:
658 641 repo_group = RepoGroup.get(repo_group_id)
659 642 prefix_match = False
660 643
661 644 # user: type search
662 645 if not prefix_match:
663 646 users, prefix_match = self._get_user_list(query)
664 647 if users:
665 648 has_specialized_search = True
666 649 for serialized_user in users:
667 650 res.append(serialized_user)
668 651 elif prefix_match:
669 652 has_specialized_search = True
670 653 res.append(no_match('No matching users found'))
671 654
672 655 # user_group: type search
673 656 if not prefix_match:
674 657 user_groups, prefix_match = self._get_user_groups_list(query)
675 658 if user_groups:
676 659 has_specialized_search = True
677 660 for serialized_user_group in user_groups:
678 661 res.append(serialized_user_group)
679 662 elif prefix_match:
680 663 has_specialized_search = True
681 664 res.append(no_match('No matching user groups found'))
682 665
683 666 # pr: type search
684 667 if not prefix_match:
685 668 pull_requests, prefix_match = self._get_pull_request_list(query)
686 669 if pull_requests:
687 670 has_specialized_search = True
688 671 for serialized_pull_request in pull_requests:
689 672 res.append(serialized_pull_request)
690 673 elif prefix_match:
691 674 has_specialized_search = True
692 675 res.append(no_match('No matching pull requests found'))
693 676
694 677 # FTS commit: type search
695 678 if not prefix_match:
696 679 commits, prefix_match = self._get_hash_commit_list(
697 680 c.auth_user, searcher, query, repo, repo_group)
698 681 if commits:
699 682 has_specialized_search = True
700 683 unique_repos = collections.OrderedDict()
701 684 for commit in commits:
702 685 repo_name = commit['repo']
703 686 unique_repos.setdefault(repo_name, []).append(commit)
704 687
705 688 for _repo, commits in unique_repos.items():
706 689 for commit in commits:
707 690 res.append(commit)
708 691 elif prefix_match:
709 692 has_specialized_search = True
710 693 res.append(no_match('No matching commits found'))
711 694
712 695 # FTS file: type search
713 696 if not prefix_match:
714 697 paths, prefix_match = self._get_path_list(
715 698 c.auth_user, searcher, query, repo, repo_group)
716 699 if paths:
717 700 has_specialized_search = True
718 701 unique_repos = collections.OrderedDict()
719 702 for path in paths:
720 703 repo_name = path['repo']
721 704 unique_repos.setdefault(repo_name, []).append(path)
722 705
723 706 for repo, paths in unique_repos.items():
724 707 for path in paths:
725 708 res.append(path)
726 709 elif prefix_match:
727 710 has_specialized_search = True
728 711 res.append(no_match('No matching files found'))
729 712
730 713 # main suggestions
731 714 if not has_specialized_search:
732 715 repo_group_name = ''
733 716 if repo_group:
734 717 repo_group_name = repo_group.group_name
735 718
736 719 for _q in self._get_default_search_queries(self.request.GET, searcher, query):
737 720 res.append(_q)
738 721
739 722 repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name)
740 723 for serialized_repo_group in repo_groups:
741 724 res.append(serialized_repo_group)
742 725
743 726 repos = self._get_repo_list(query, repo_group_name=repo_group_name)
744 727 for serialized_repo in repos:
745 728 res.append(serialized_repo)
746 729
747 730 if not repos and not repo_groups:
748 731 res.append(no_match('No matches found'))
749 732
750 733 return {'suggestions': res}
751 734
752 735 @LoginRequired()
753 @view_config(
754 route_name='home', request_method='GET',
755 renderer='rhodecode:templates/index.mako')
756 736 def main_page(self):
757 737 c = self.load_default_context()
758 738 c.repo_group = None
759 739 return self._get_template_context(c)
760 740
761 741 def _main_page_repo_groups_data(self, repo_group_id):
762 742 column_map = {
763 743 'name': 'group_name_hash',
764 744 'desc': 'group_description',
765 745 'last_change': 'updated_on',
766 746 'owner': 'user_username',
767 747 }
768 748 draw, start, limit = self._extract_chunk(self.request)
769 749 search_q, order_by, order_dir = self._extract_ordering(
770 750 self.request, column_map=column_map)
771 751 return RepoGroupModel().get_repo_groups_data_table(
772 752 draw, start, limit,
773 753 search_q, order_by, order_dir,
774 754 self._rhodecode_user, repo_group_id)
775 755
776 756 def _main_page_repos_data(self, repo_group_id):
777 757 column_map = {
778 758 'name': 'repo_name',
779 759 'desc': 'description',
780 760 'last_change': 'updated_on',
781 761 'owner': 'user_username',
782 762 }
783 763 draw, start, limit = self._extract_chunk(self.request)
784 764 search_q, order_by, order_dir = self._extract_ordering(
785 765 self.request, column_map=column_map)
786 766 return RepoModel().get_repos_data_table(
787 767 draw, start, limit,
788 768 search_q, order_by, order_dir,
789 769 self._rhodecode_user, repo_group_id)
790 770
791 771 @LoginRequired()
792 @view_config(
793 route_name='main_page_repo_groups_data',
794 request_method='GET', renderer='json_ext', xhr=True)
795 772 def main_page_repo_groups_data(self):
796 773 self.load_default_context()
797 774 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
798 775
799 776 if repo_group_id:
800 777 group = RepoGroup.get_or_404(repo_group_id)
801 778 _perms = AuthUser.repo_group_read_perms
802 779 if not HasRepoGroupPermissionAny(*_perms)(
803 780 group.group_name, 'user is allowed to list repo group children'):
804 781 raise HTTPNotFound()
805 782
806 783 return self._main_page_repo_groups_data(repo_group_id)
807 784
808 785 @LoginRequired()
809 @view_config(
810 route_name='main_page_repos_data',
811 request_method='GET', renderer='json_ext', xhr=True)
812 786 def main_page_repos_data(self):
813 787 self.load_default_context()
814 788 repo_group_id = safe_int(self.request.GET.get('repo_group_id'))
815 789
816 790 if repo_group_id:
817 791 group = RepoGroup.get_or_404(repo_group_id)
818 792 _perms = AuthUser.repo_group_read_perms
819 793 if not HasRepoGroupPermissionAny(*_perms)(
820 794 group.group_name, 'user is allowed to list repo group children'):
821 795 raise HTTPNotFound()
822 796
823 797 return self._main_page_repos_data(repo_group_id)
824 798
825 799 @LoginRequired()
826 800 @HasRepoGroupPermissionAnyDecorator(*AuthUser.repo_group_read_perms)
827 @view_config(
828 route_name='repo_group_home', request_method='GET',
829 renderer='rhodecode:templates/index_repo_group.mako')
830 @view_config(
831 route_name='repo_group_home_slash', request_method='GET',
832 renderer='rhodecode:templates/index_repo_group.mako')
833 801 def repo_group_main_page(self):
834 802 c = self.load_default_context()
835 803 c.repo_group = self.request.db_repo_group
836 804 return self._get_template_context(c)
837 805
838 806 @LoginRequired()
839 807 @CSRFRequired()
840 @view_config(
841 route_name='markup_preview', request_method='POST',
842 renderer='string', xhr=True)
843 808 def markup_preview(self):
844 809 # Technically a CSRF token is not needed as no state changes with this
845 810 # call. However, as this is a POST is better to have it, so automated
846 811 # tools don't flag it as potential CSRF.
847 812 # Post is required because the payload could be bigger than the maximum
848 813 # allowed by GET.
849 814
850 815 text = self.request.POST.get('text')
851 816 renderer = self.request.POST.get('renderer') or 'rst'
852 817 if text:
853 818 return h.render(text, renderer=renderer, mentions=True)
854 819 return ''
855 820
856 821 @LoginRequired()
857 822 @CSRFRequired()
858 @view_config(
859 route_name='file_preview', request_method='POST',
860 renderer='string', xhr=True)
861 823 def file_preview(self):
862 824 # Technically a CSRF token is not needed as no state changes with this
863 825 # call. However, as this is a POST is better to have it, so automated
864 826 # tools don't flag it as potential CSRF.
865 827 # Post is required because the payload could be bigger than the maximum
866 828 # allowed by GET.
867 829
868 830 text = self.request.POST.get('text')
869 831 file_path = self.request.POST.get('file_path')
870 832
871 833 renderer = h.renderer_from_filename(file_path)
872 834
873 835 if renderer:
874 836 return h.render(text, renderer=renderer, mentions=True)
875 837 else:
876 838 self.load_default_context()
877 839 _render = self.request.get_partial_renderer(
878 840 'rhodecode:templates/files/file_content.mako')
879 841
880 842 lines = filenode_as_lines_tokens(FileNode(file_path, text))
881 843
882 844 return _render('render_lines', lines)
883 845
884 846 @LoginRequired()
885 847 @CSRFRequired()
886 @view_config(
887 route_name='store_user_session_value', request_method='POST',
888 renderer='string', xhr=True)
889 848 def store_user_session_attr(self):
890 849 key = self.request.POST.get('key')
891 850 val = self.request.POST.get('val')
892 851
893 852 existing_value = self.request.session.get(key)
894 853 if existing_value != val:
895 854 self.request.session[key] = val
896 855
897 856 return 'stored:{}:{}'.format(key, val)
@@ -1,45 +1,67 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2018-2020 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 def includeme(config):
23
23 from rhodecode.apps.hovercards.views import HoverCardsView, HoverCardsRepoView
24 24 config.add_route(
25 25 name='hovercard_user',
26 26 pattern='/_hovercard/user/{user_id}')
27 config.add_view(
28 HoverCardsView,
29 attr='hovercard_user',
30 route_name='hovercard_user', request_method='GET', xhr=True,
31 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
27 32
28 33 config.add_route(
29 34 name='hovercard_username',
30 35 pattern='/_hovercard/username/{username}')
36 config.add_view(
37 HoverCardsView,
38 attr='hovercard_username',
39 route_name='hovercard_username', request_method='GET', xhr=True,
40 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
31 41
32 42 config.add_route(
33 43 name='hovercard_user_group',
34 44 pattern='/_hovercard/user_group/{user_group_id}')
45 config.add_view(
46 HoverCardsView,
47 attr='hovercard_user_group',
48 route_name='hovercard_user_group', request_method='GET', xhr=True,
49 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
35 50
36 51 config.add_route(
37 52 name='hovercard_pull_request',
38 53 pattern='/_hovercard/pull_request/{pull_request_id}')
54 config.add_view(
55 HoverCardsView,
56 attr='hovercard_pull_request',
57 route_name='hovercard_pull_request', request_method='GET', xhr=True,
58 renderer='rhodecode:templates/hovercards/hovercard_pull_request.mako')
39 59
40 60 config.add_route(
41 61 name='hovercard_repo_commit',
42 62 pattern='/_hovercard/commit/{repo_name:.*?[^/]}/{commit_id}', repo_route=True)
43
44 # Scan module for configuration decorators.
45 config.scan('.views', ignore='.tests')
63 config.add_view(
64 HoverCardsRepoView,
65 attr='hovercard_repo_commit',
66 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
67 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
@@ -1,123 +1,108 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 re
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound
26 from pyramid.view import view_config
26
27 27
28 28 from rhodecode.apps._base import BaseAppView, RepoAppView
29 29 from rhodecode.lib import helpers as h
30 30 from rhodecode.lib.auth import (
31 31 LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, CSRFRequired,
32 32 HasRepoPermissionAnyDecorator)
33 33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 34 from rhodecode.lib.index import searcher_from_config
35 35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, EmptyRepositoryError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39 from rhodecode.model.db import (
40 40 func, true, or_, case, in_filter_generator, Repository, RepoGroup, User, UserGroup, PullRequest)
41 41 from rhodecode.model.repo import RepoModel
42 42 from rhodecode.model.repo_group import RepoGroupModel
43 43 from rhodecode.model.scm import RepoGroupList, RepoList
44 44 from rhodecode.model.user import UserModel
45 45 from rhodecode.model.user_group import UserGroupModel
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HoverCardsView(BaseAppView):
51 51
52 52 def load_default_context(self):
53 53 c = self._get_local_tmpl_context()
54 54 return c
55 55
56 56 @LoginRequired()
57 @view_config(
58 route_name='hovercard_user', request_method='GET', xhr=True,
59 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
60 57 def hovercard_user(self):
61 58 c = self.load_default_context()
62 59 user_id = self.request.matchdict['user_id']
63 60 c.user = User.get_or_404(user_id)
64 61 return self._get_template_context(c)
65 62
66 63 @LoginRequired()
67 @view_config(
68 route_name='hovercard_username', request_method='GET', xhr=True,
69 renderer='rhodecode:templates/hovercards/hovercard_user.mako')
70 64 def hovercard_username(self):
71 65 c = self.load_default_context()
72 66 username = self.request.matchdict['username']
73 67 c.user = User.get_by_username(username)
74 68 if not c.user:
75 69 raise HTTPNotFound()
76 70
77 71 return self._get_template_context(c)
78 72
79 73 @LoginRequired()
80 @view_config(
81 route_name='hovercard_user_group', request_method='GET', xhr=True,
82 renderer='rhodecode:templates/hovercards/hovercard_user_group.mako')
83 74 def hovercard_user_group(self):
84 75 c = self.load_default_context()
85 76 user_group_id = self.request.matchdict['user_group_id']
86 77 c.user_group = UserGroup.get_or_404(user_group_id)
87 78 return self._get_template_context(c)
88 79
89 80 @LoginRequired()
90 @view_config(
91 route_name='hovercard_pull_request', request_method='GET', xhr=True,
92 renderer='rhodecode:templates/hovercards/hovercard_pull_request.mako')
93 81 def hovercard_pull_request(self):
94 82 c = self.load_default_context()
95 83 c.pull_request = PullRequest.get_or_404(
96 84 self.request.matchdict['pull_request_id'])
97 85 perms = ['repository.read', 'repository.write', 'repository.admin']
98 86 c.can_view_pr = h.HasRepoPermissionAny(*perms)(
99 87 c.pull_request.target_repo.repo_name)
100 88 return self._get_template_context(c)
101 89
102 90
103 91 class HoverCardsRepoView(RepoAppView):
104 92 def load_default_context(self):
105 93 c = self._get_local_tmpl_context()
106 94 return c
107 95
108 96 @LoginRequired()
109 97 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin')
110 @view_config(
111 route_name='hovercard_repo_commit', request_method='GET', xhr=True,
112 renderer='rhodecode:templates/hovercards/hovercard_repo_commit.mako')
113 98 def hovercard_repo_commit(self):
114 99 c = self.load_default_context()
115 100 commit_id = self.request.matchdict['commit_id']
116 101 pre_load = ['author', 'branch', 'date', 'message']
117 102 try:
118 103 c.commit = self.rhodecode_vcs_repo.get_commit(
119 104 commit_id=commit_id, pre_load=pre_load)
120 105 except (CommitDoesNotExistError, EmptyRepositoryError):
121 106 raise HTTPNotFound()
122 107
123 108 return self._get_template_context(c)
@@ -1,53 +1,102 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def admin_routes(config):
26 from rhodecode.apps.journal.views import JournalView
26 27
27 28 config.add_route(
28 29 name='journal', pattern='/journal')
30 config.add_view(
31 JournalView,
32 attr='journal',
33 route_name='journal', request_method='GET',
34 renderer=None)
35
29 36 config.add_route(
30 37 name='journal_rss', pattern='/journal/rss')
38 config.add_view(
39 JournalView,
40 attr='journal_rss',
41 route_name='journal_rss', request_method='GET',
42 renderer=None)
43
31 44 config.add_route(
32 45 name='journal_atom', pattern='/journal/atom')
46 config.add_view(
47 JournalView,
48 attr='journal_atom',
49 route_name='journal_atom', request_method='GET',
50 renderer=None)
33 51
34 52 config.add_route(
35 53 name='journal_public', pattern='/public_journal')
54 config.add_view(
55 JournalView,
56 attr='journal_public',
57 route_name='journal_public', request_method='GET',
58 renderer=None)
59
36 60 config.add_route(
37 61 name='journal_public_atom', pattern='/public_journal/atom')
62 config.add_view(
63 JournalView,
64 attr='journal_public_atom',
65 route_name='journal_public_atom', request_method='GET',
66 renderer=None)
67
38 68 config.add_route(
39 69 name='journal_public_atom_old', pattern='/public_journal_atom')
70 config.add_view(
71 JournalView,
72 attr='journal_public_atom',
73 route_name='journal_public_atom_old', request_method='GET',
74 renderer=None)
40 75
41 76 config.add_route(
42 77 name='journal_public_rss', pattern='/public_journal/rss')
78 config.add_view(
79 JournalView,
80 attr='journal_public_rss',
81 route_name='journal_public_rss', request_method='GET',
82 renderer=None)
83
43 84 config.add_route(
44 85 name='journal_public_rss_old', pattern='/public_journal_rss')
86 config.add_view(
87 JournalView,
88 attr='journal_public_rss',
89 route_name='journal_public_rss_old', request_method='GET',
90 renderer=None)
45 91
46 92 config.add_route(
47 93 name='toggle_following', pattern='/toggle_following')
94 config.add_view(
95 JournalView,
96 attr='toggle_following',
97 route_name='toggle_following', request_method='POST',
98 renderer='json_ext')
48 99
49 100
50 101 def includeme(config):
51 102 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
52 # Scan module for configuration decorators.
53 config.scan('.views', ignore='.tests')
@@ -1,389 +1,364 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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
22 21 import logging
23 22 import itertools
24 23
25
26
27 from pyramid.view import view_config
28 24 from pyramid.httpexceptions import HTTPBadRequest
29 25 from pyramid.response import Response
30 26 from pyramid.renderers import render
31 27
32 28 from rhodecode.apps._base import BaseAppView
33 29 from rhodecode.model.db import (
34 30 or_, joinedload, Repository, UserLog, UserFollowing, User, UserApiKeys)
35 31 from rhodecode.model.meta import Session
36 32 import rhodecode.lib.helpers as h
37 33 from rhodecode.lib.helpers import SqlPage
38 34 from rhodecode.lib.user_log_filter import user_log_filter
39 35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, HasRepoPermissionAny
40 36 from rhodecode.lib.utils2 import safe_int, AttributeDict, md5_safe
41 37 from rhodecode.lib.feedgenerator.feedgenerator import Atom1Feed, Rss201rev2Feed
42 38 from rhodecode.model.scm import ScmModel
43 39
44 40 log = logging.getLogger(__name__)
45 41
46 42
47 43 class JournalView(BaseAppView):
48 44
49 45 def load_default_context(self):
50 46 c = self._get_local_tmpl_context(include_app_defaults=True)
51 47
52 48 self._load_defaults(c.rhodecode_name)
53 49
54 50 # TODO(marcink): what is this, why we need a global register ?
55 51 c.search_term = self.request.GET.get('filter') or ''
56 52 return c
57 53
58 54 def _get_config(self, rhodecode_name):
59 55 import rhodecode
60 56 config = rhodecode.CONFIG
61 57
62 58 return {
63 59 'language': 'en-us',
64 60 'feed_ttl': '5', # TTL of feed,
65 61 'feed_items_per_page':
66 62 safe_int(config.get('rss_items_per_page', 20)),
67 63 'rhodecode_name': rhodecode_name
68 64 }
69 65
70 66 def _load_defaults(self, rhodecode_name):
71 67 config = self._get_config(rhodecode_name)
72 68 # common values for feeds
73 69 self.language = config["language"]
74 70 self.ttl = config["feed_ttl"]
75 71 self.feed_items_per_page = config['feed_items_per_page']
76 72 self.rhodecode_name = config['rhodecode_name']
77 73
78 74 def _get_daily_aggregate(self, journal):
79 75 groups = []
80 76 for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
81 77 user_group = []
82 78 # groupby username if it's a present value, else
83 79 # fallback to journal username
84 80 for _, g2 in itertools.groupby(
85 81 list(g), lambda x: x.user.username if x.user else x.username):
86 82 l = list(g2)
87 83 user_group.append((l[0].user, l))
88 84
89 85 groups.append((k, user_group,))
90 86
91 87 return groups
92 88
93 89 def _get_journal_data(self, following_repos, search_term):
94 90 repo_ids = [x.follows_repository.repo_id for x in following_repos
95 91 if x.follows_repository is not None]
96 92 user_ids = [x.follows_user.user_id for x in following_repos
97 93 if x.follows_user is not None]
98 94
99 95 filtering_criterion = None
100 96
101 97 if repo_ids and user_ids:
102 98 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
103 99 UserLog.user_id.in_(user_ids))
104 100 if repo_ids and not user_ids:
105 101 filtering_criterion = UserLog.repository_id.in_(repo_ids)
106 102 if not repo_ids and user_ids:
107 103 filtering_criterion = UserLog.user_id.in_(user_ids)
108 104 if filtering_criterion is not None:
109 105 journal = Session().query(UserLog)\
110 106 .options(joinedload(UserLog.user))\
111 107 .options(joinedload(UserLog.repository))
112 108 # filter
113 109 try:
114 110 journal = user_log_filter(journal, search_term)
115 111 except Exception:
116 112 # we want this to crash for now
117 113 raise
118 114 journal = journal.filter(filtering_criterion)\
119 115 .order_by(UserLog.action_date.desc())
120 116 else:
121 117 journal = []
122 118
123 119 return journal
124 120
125 121 def feed_uid(self, entry_id):
126 122 return '{}:{}'.format('journal', md5_safe(entry_id))
127 123
128 124 def _atom_feed(self, repos, search_term, public=True):
129 125 _ = self.request.translate
130 126 journal = self._get_journal_data(repos, search_term)
131 127 if public:
132 128 _link = h.route_url('journal_public_atom')
133 129 _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
134 130 'atom feed')
135 131 else:
136 132 _link = h.route_url('journal_atom')
137 133 _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
138 134
139 135 feed = Atom1Feed(
140 136 title=_desc, link=_link, description=_desc,
141 137 language=self.language, ttl=self.ttl)
142 138
143 139 for entry in journal[:self.feed_items_per_page]:
144 140 user = entry.user
145 141 if user is None:
146 142 # fix deleted users
147 143 user = AttributeDict({'short_contact': entry.username,
148 144 'email': '',
149 145 'full_contact': ''})
150 146 action, action_extra, ico = h.action_parser(
151 147 self.request, entry, feed=True)
152 148 title = "%s - %s %s" % (user.short_contact, action(),
153 149 entry.repository.repo_name)
154 150 desc = action_extra()
155 151 _url = h.route_url('home')
156 152 if entry.repository is not None:
157 153 _url = h.route_url('repo_commits',
158 154 repo_name=entry.repository.repo_name)
159 155
160 156 feed.add_item(
161 157 unique_id=self.feed_uid(entry.user_log_id),
162 158 title=title,
163 159 pubdate=entry.action_date,
164 160 link=_url,
165 161 author_email=user.email,
166 162 author_name=user.full_contact,
167 163 description=desc)
168 164
169 165 response = Response(feed.writeString('utf-8'))
170 166 response.content_type = feed.content_type
171 167 return response
172 168
173 169 def _rss_feed(self, repos, search_term, public=True):
174 170 _ = self.request.translate
175 171 journal = self._get_journal_data(repos, search_term)
176 172 if public:
177 173 _link = h.route_url('journal_public_atom')
178 174 _desc = '%s %s %s' % (
179 175 self.rhodecode_name, _('public journal'), 'rss feed')
180 176 else:
181 177 _link = h.route_url('journal_atom')
182 178 _desc = '%s %s %s' % (
183 179 self.rhodecode_name, _('journal'), 'rss feed')
184 180
185 181 feed = Rss201rev2Feed(
186 182 title=_desc, link=_link, description=_desc,
187 183 language=self.language, ttl=self.ttl)
188 184
189 185 for entry in journal[:self.feed_items_per_page]:
190 186 user = entry.user
191 187 if user is None:
192 188 # fix deleted users
193 189 user = AttributeDict({'short_contact': entry.username,
194 190 'email': '',
195 191 'full_contact': ''})
196 192 action, action_extra, ico = h.action_parser(
197 193 self.request, entry, feed=True)
198 194 title = "%s - %s %s" % (user.short_contact, action(),
199 195 entry.repository.repo_name)
200 196 desc = action_extra()
201 197 _url = h.route_url('home')
202 198 if entry.repository is not None:
203 199 _url = h.route_url('repo_commits',
204 200 repo_name=entry.repository.repo_name)
205 201
206 202 feed.add_item(
207 203 unique_id=self.feed_uid(entry.user_log_id),
208 204 title=title,
209 205 pubdate=entry.action_date,
210 206 link=_url,
211 207 author_email=user.email,
212 208 author_name=user.full_contact,
213 209 description=desc)
214 210
215 211 response = Response(feed.writeString('utf-8'))
216 212 response.content_type = feed.content_type
217 213 return response
218 214
219 215 @LoginRequired()
220 216 @NotAnonymous()
221 @view_config(
222 route_name='journal', request_method='GET',
223 renderer=None)
224 217 def journal(self):
225 218 c = self.load_default_context()
226 219
227 220 p = safe_int(self.request.GET.get('page', 1), 1)
228 221 c.user = User.get(self._rhodecode_user.user_id)
229 222 following = Session().query(UserFollowing)\
230 223 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
231 224 .options(joinedload(UserFollowing.follows_repository))\
232 225 .all()
233 226
234 227 journal = self._get_journal_data(following, c.search_term)
235 228
236 229 def url_generator(page_num):
237 230 query_params = {
238 231 'page': page_num,
239 232 'filter': c.search_term
240 233 }
241 234 return self.request.current_route_path(_query=query_params)
242 235
243 236 c.journal_pager = SqlPage(
244 237 journal, page=p, items_per_page=20, url_maker=url_generator)
245 238 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
246 239
247 240 c.journal_data = render(
248 241 'rhodecode:templates/journal/journal_data.mako',
249 242 self._get_template_context(c), self.request)
250 243
251 244 if self.request.is_xhr:
252 245 return Response(c.journal_data)
253 246
254 247 html = render(
255 248 'rhodecode:templates/journal/journal.mako',
256 249 self._get_template_context(c), self.request)
257 250 return Response(html)
258 251
259 252 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
260 253 @NotAnonymous()
261 @view_config(
262 route_name='journal_atom', request_method='GET',
263 renderer=None)
264 254 def journal_atom(self):
265 255 """
266 256 Produce an atom-1.0 feed via feedgenerator module
267 257 """
268 258 c = self.load_default_context()
269 259 following_repos = Session().query(UserFollowing)\
270 260 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
271 261 .options(joinedload(UserFollowing.follows_repository))\
272 262 .all()
273 263 return self._atom_feed(following_repos, c.search_term, public=False)
274 264
275 265 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
276 266 @NotAnonymous()
277 @view_config(
278 route_name='journal_rss', request_method='GET',
279 renderer=None)
280 267 def journal_rss(self):
281 268 """
282 269 Produce an rss feed via feedgenerator module
283 270 """
284 271 c = self.load_default_context()
285 272 following_repos = Session().query(UserFollowing)\
286 273 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
287 274 .options(joinedload(UserFollowing.follows_repository))\
288 275 .all()
289 276 return self._rss_feed(following_repos, c.search_term, public=False)
290 277
291 278 @LoginRequired()
292 @NotAnonymous()
293 @CSRFRequired()
294 @view_config(
295 route_name='toggle_following', request_method='POST',
296 renderer='json_ext')
297 def toggle_following(self):
298 user_id = self.request.POST.get('follows_user_id')
299 if user_id:
300 try:
301 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
302 Session().commit()
303 return 'ok'
304 except Exception:
305 raise HTTPBadRequest()
306
307 repo_id = self.request.POST.get('follows_repo_id')
308 repo = Repository.get_or_404(repo_id)
309 perm_set = ['repository.read', 'repository.write', 'repository.admin']
310 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'RepoWatch check')
311 if repo and has_perm:
312 try:
313 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
314 Session().commit()
315 return 'ok'
316 except Exception:
317 raise HTTPBadRequest()
318
319 raise HTTPBadRequest()
320
321 @LoginRequired()
322 @view_config(
323 route_name='journal_public', request_method='GET',
324 renderer=None)
325 279 def journal_public(self):
326 280 c = self.load_default_context()
327 281 # Return a rendered template
328 282 p = safe_int(self.request.GET.get('page', 1), 1)
329 283
330 284 c.following = Session().query(UserFollowing)\
331 285 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
332 286 .options(joinedload(UserFollowing.follows_repository))\
333 287 .all()
334 288
335 289 journal = self._get_journal_data(c.following, c.search_term)
336 290
337 291 def url_generator(page_num):
338 292 query_params = {
339 293 'page': page_num
340 294 }
341 295 return self.request.current_route_path(_query=query_params)
342 296
343 297 c.journal_pager = SqlPage(
344 298 journal, page=p, items_per_page=20, url_maker=url_generator)
345 299 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
346 300
347 301 c.journal_data = render(
348 302 'rhodecode:templates/journal/journal_data.mako',
349 303 self._get_template_context(c), self.request)
350 304
351 305 if self.request.is_xhr:
352 306 return Response(c.journal_data)
353 307
354 308 html = render(
355 309 'rhodecode:templates/journal/public_journal.mako',
356 310 self._get_template_context(c), self.request)
357 311 return Response(html)
358 312
359 313 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
360 @view_config(
361 route_name='journal_public_atom', request_method='GET',
362 renderer=None)
363 314 def journal_public_atom(self):
364 315 """
365 316 Produce an atom-1.0 feed via feedgenerator module
366 317 """
367 318 c = self.load_default_context()
368 319 following_repos = Session().query(UserFollowing)\
369 320 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
370 321 .options(joinedload(UserFollowing.follows_repository))\
371 322 .all()
372 323
373 324 return self._atom_feed(following_repos, c.search_term)
374 325
375 326 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
376 @view_config(
377 route_name='journal_public_rss', request_method='GET',
378 renderer=None)
379 327 def journal_public_rss(self):
380 328 """
381 329 Produce an rss2 feed via feedgenerator module
382 330 """
383 331 c = self.load_default_context()
384 332 following_repos = Session().query(UserFollowing)\
385 333 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
386 334 .options(joinedload(UserFollowing.follows_repository))\
387 335 .all()
388 336
389 337 return self._rss_feed(following_repos, c.search_term)
338
339 @LoginRequired()
340 @NotAnonymous()
341 @CSRFRequired()
342 def toggle_following(self):
343 user_id = self.request.POST.get('follows_user_id')
344 if user_id:
345 try:
346 ScmModel().toggle_following_user(user_id, self._rhodecode_user.user_id)
347 Session().commit()
348 return 'ok'
349 except Exception:
350 raise HTTPBadRequest()
351
352 repo_id = self.request.POST.get('follows_repo_id')
353 repo = Repository.get_or_404(repo_id)
354 perm_set = ['repository.read', 'repository.write', 'repository.admin']
355 has_perm = HasRepoPermissionAny(*perm_set)(repo.repo_name, 'RepoWatch check')
356 if repo and has_perm:
357 try:
358 ScmModel().toggle_following_repo(repo_id, self._rhodecode_user.user_id)
359 Session().commit()
360 return 'ok'
361 except Exception:
362 raise HTTPBadRequest()
363
364 raise HTTPBadRequest()
@@ -1,44 +1,79 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def includeme(config):
26
26 from rhodecode.apps.login.views import LoginView
27
27 28 config.add_route(
28 29 name='login',
29 30 pattern=ADMIN_PREFIX + '/login')
31 config.add_view(
32 LoginView,
33 attr='login',
34 route_name='login', request_method='GET',
35 renderer='rhodecode:templates/login.mako')
36 config.add_view(
37 LoginView,
38 attr='login_post',
39 route_name='login', request_method='POST',
40 renderer='rhodecode:templates/login.mako')
41
30 42 config.add_route(
31 43 name='logout',
32 44 pattern=ADMIN_PREFIX + '/logout')
45 config.add_view(
46 LoginView,
47 attr='logout',
48 route_name='logout', request_method='POST')
49
33 50 config.add_route(
34 51 name='register',
35 52 pattern=ADMIN_PREFIX + '/register')
53 config.add_view(
54 LoginView,
55 attr='register',
56 route_name='register', request_method='GET',
57 renderer='rhodecode:templates/register.mako')
58 config.add_view(
59 LoginView,
60 attr='register_post',
61 route_name='register', request_method='POST',
62 renderer='rhodecode:templates/register.mako')
63
36 64 config.add_route(
37 65 name='reset_password',
38 66 pattern=ADMIN_PREFIX + '/password_reset')
67 config.add_view(
68 LoginView,
69 attr='password_reset',
70 route_name='reset_password', request_method=('GET', 'POST'),
71 renderer='rhodecode:templates/password_reset.mako')
72
39 73 config.add_route(
40 74 name='reset_password_confirmation',
41 75 pattern=ADMIN_PREFIX + '/password_reset_confirmation')
42
43 # Scan module for configuration decorators.
44 config.scan('.views', ignore='.tests')
76 config.add_view(
77 LoginView,
78 attr='password_reset_confirmation',
79 route_name='reset_password_confirmation', request_method='GET')
@@ -1,489 +1,470 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import collections
23 23 import datetime
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import logging
27 27 import urlparse
28 28 import requests
29 29
30 30 from pyramid.httpexceptions import HTTPFound
31 from pyramid.view import view_config
31
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import UserRegistered, trigger
37 37 from rhodecode.lib import helpers as h
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.auth import (
40 40 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
41 41 from rhodecode.lib.base import get_ip_addr
42 42 from rhodecode.lib.exceptions import UserCreationError
43 43 from rhodecode.lib.utils2 import safe_str
44 44 from rhodecode.model.db import User, UserApiKeys
45 45 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.auth_token import AuthTokenModel
48 48 from rhodecode.model.settings import SettingsModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.translation import _
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55 CaptchaData = collections.namedtuple(
56 56 'CaptchaData', 'active, private_key, public_key')
57 57
58 58
59 59 def store_user_in_session(session, username, remember=False):
60 60 user = User.get_by_username(username, case_insensitive=True)
61 61 auth_user = AuthUser(user.user_id)
62 62 auth_user.set_authenticated()
63 63 cs = auth_user.get_cookie_store()
64 64 session['rhodecode_user'] = cs
65 65 user.update_lastlogin()
66 66 Session().commit()
67 67
68 68 # If they want to be remembered, update the cookie
69 69 if remember:
70 70 _year = (datetime.datetime.now() +
71 71 datetime.timedelta(seconds=60 * 60 * 24 * 365))
72 72 session._set_cookie_expires(_year)
73 73
74 74 session.save()
75 75
76 76 safe_cs = cs.copy()
77 77 safe_cs['password'] = '****'
78 78 log.info('user %s is now authenticated and stored in '
79 79 'session, session attrs %s', username, safe_cs)
80 80
81 81 # dumps session attrs back to cookie
82 82 session._update_cookie_out()
83 83 # we set new cookie
84 84 headers = None
85 85 if session.request['set_cookie']:
86 86 # send set-cookie headers back to response to update cookie
87 87 headers = [('Set-Cookie', session.request['cookie_out'])]
88 88 return headers
89 89
90 90
91 91 def get_came_from(request):
92 92 came_from = safe_str(request.GET.get('came_from', ''))
93 93 parsed = urlparse.urlparse(came_from)
94 94 allowed_schemes = ['http', 'https']
95 95 default_came_from = h.route_path('home')
96 96 if parsed.scheme and parsed.scheme not in allowed_schemes:
97 97 log.error('Suspicious URL scheme detected %s for url %s',
98 98 parsed.scheme, parsed)
99 99 came_from = default_came_from
100 100 elif parsed.netloc and request.host != parsed.netloc:
101 101 log.error('Suspicious NETLOC detected %s for url %s server url '
102 102 'is: %s', parsed.netloc, parsed, request.host)
103 103 came_from = default_came_from
104 104 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
105 105 log.error('Header injection detected `%s` for url %s server url ',
106 106 parsed.path, parsed)
107 107 came_from = default_came_from
108 108
109 109 return came_from or default_came_from
110 110
111 111
112 112 class LoginView(BaseAppView):
113 113
114 114 def load_default_context(self):
115 115 c = self._get_local_tmpl_context()
116 116 c.came_from = get_came_from(self.request)
117
118 117 return c
119 118
120 119 def _get_captcha_data(self):
121 120 settings = SettingsModel().get_all_settings()
122 121 private_key = settings.get('rhodecode_captcha_private_key')
123 122 public_key = settings.get('rhodecode_captcha_public_key')
124 123 active = bool(private_key)
125 124 return CaptchaData(
126 125 active=active, private_key=private_key, public_key=public_key)
127 126
128 127 def validate_captcha(self, private_key):
129 128
130 129 captcha_rs = self.request.POST.get('g-recaptcha-response')
131 130 url = "https://www.google.com/recaptcha/api/siteverify"
132 131 params = {
133 132 'secret': private_key,
134 133 'response': captcha_rs,
135 134 'remoteip': get_ip_addr(self.request.environ)
136 135 }
137 136 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
138 137 verify_rs = verify_rs.json()
139 138 captcha_status = verify_rs.get('success', False)
140 139 captcha_errors = verify_rs.get('error-codes', [])
141 140 if not isinstance(captcha_errors, list):
142 141 captcha_errors = [captcha_errors]
143 142 captcha_errors = ', '.join(captcha_errors)
144 143 captcha_message = ''
145 144 if captcha_status is False:
146 145 captcha_message = "Bad captcha. Errors: {}".format(
147 146 captcha_errors)
148 147
149 148 return captcha_status, captcha_message
150 149
151 @view_config(
152 route_name='login', request_method='GET',
153 renderer='rhodecode:templates/login.mako')
154 150 def login(self):
155 151 c = self.load_default_context()
156 152 auth_user = self._rhodecode_user
157 153
158 154 # redirect if already logged in
159 155 if (auth_user.is_authenticated and
160 156 not auth_user.is_default and auth_user.ip_allowed):
161 157 raise HTTPFound(c.came_from)
162 158
163 159 # check if we use headers plugin, and try to login using it.
164 160 try:
165 161 log.debug('Running PRE-AUTH for headers based authentication')
166 162 auth_info = authenticate(
167 163 '', '', self.request.environ, HTTP_TYPE, skip_missing=True)
168 164 if auth_info:
169 165 headers = store_user_in_session(
170 166 self.session, auth_info.get('username'))
171 167 raise HTTPFound(c.came_from, headers=headers)
172 168 except UserCreationError as e:
173 169 log.error(e)
174 170 h.flash(e, category='error')
175 171
176 172 return self._get_template_context(c)
177 173
178 @view_config(
179 route_name='login', request_method='POST',
180 renderer='rhodecode:templates/login.mako')
181 174 def login_post(self):
182 175 c = self.load_default_context()
183 176
184 177 login_form = LoginForm(self.request.translate)()
185 178
186 179 try:
187 180 self.session.invalidate()
188 181 form_result = login_form.to_python(self.request.POST)
189 182 # form checks for username/password, now we're authenticated
190 183 headers = store_user_in_session(
191 184 self.session,
192 185 username=form_result['username'],
193 186 remember=form_result['remember'])
194 187 log.debug('Redirecting to "%s" after login.', c.came_from)
195 188
196 189 audit_user = audit_logger.UserWrap(
197 190 username=self.request.POST.get('username'),
198 191 ip_addr=self.request.remote_addr)
199 192 action_data = {'user_agent': self.request.user_agent}
200 193 audit_logger.store_web(
201 194 'user.login.success', action_data=action_data,
202 195 user=audit_user, commit=True)
203 196
204 197 raise HTTPFound(c.came_from, headers=headers)
205 198 except formencode.Invalid as errors:
206 199 defaults = errors.value
207 200 # remove password from filling in form again
208 201 defaults.pop('password', None)
209 202 render_ctx = {
210 203 'errors': errors.error_dict,
211 204 'defaults': defaults,
212 205 }
213 206
214 207 audit_user = audit_logger.UserWrap(
215 208 username=self.request.POST.get('username'),
216 209 ip_addr=self.request.remote_addr)
217 210 action_data = {'user_agent': self.request.user_agent}
218 211 audit_logger.store_web(
219 212 'user.login.failure', action_data=action_data,
220 213 user=audit_user, commit=True)
221 214 return self._get_template_context(c, **render_ctx)
222 215
223 216 except UserCreationError as e:
224 217 # headers auth or other auth functions that create users on
225 218 # the fly can throw this exception signaling that there's issue
226 219 # with user creation, explanation should be provided in
227 220 # Exception itself
228 221 h.flash(e, category='error')
229 222 return self._get_template_context(c)
230 223
231 224 @CSRFRequired()
232 @view_config(route_name='logout', request_method='POST')
233 225 def logout(self):
234 226 auth_user = self._rhodecode_user
235 227 log.info('Deleting session for user: `%s`', auth_user)
236 228
237 229 action_data = {'user_agent': self.request.user_agent}
238 230 audit_logger.store_web(
239 231 'user.logout', action_data=action_data,
240 232 user=auth_user, commit=True)
241 233 self.session.delete()
242 234 return HTTPFound(h.route_path('home'))
243 235
244 236 @HasPermissionAnyDecorator(
245 237 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
246 @view_config(
247 route_name='register', request_method='GET',
248 renderer='rhodecode:templates/register.mako',)
249 238 def register(self, defaults=None, errors=None):
250 239 c = self.load_default_context()
251 240 defaults = defaults or {}
252 241 errors = errors or {}
253 242
254 243 settings = SettingsModel().get_all_settings()
255 244 register_message = settings.get('rhodecode_register_message') or ''
256 245 captcha = self._get_captcha_data()
257 246 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
258 247 .AuthUser().permissions['global']
259 248
260 249 render_ctx = self._get_template_context(c)
261 250 render_ctx.update({
262 251 'defaults': defaults,
263 252 'errors': errors,
264 253 'auto_active': auto_active,
265 254 'captcha_active': captcha.active,
266 255 'captcha_public_key': captcha.public_key,
267 256 'register_message': register_message,
268 257 })
269 258 return render_ctx
270 259
271 260 @HasPermissionAnyDecorator(
272 261 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
273 @view_config(
274 route_name='register', request_method='POST',
275 renderer='rhodecode:templates/register.mako')
276 262 def register_post(self):
277 263 from rhodecode.authentication.plugins import auth_rhodecode
278 264
279 265 self.load_default_context()
280 266 captcha = self._get_captcha_data()
281 267 auto_active = 'hg.register.auto_activate' in User.get_default_user()\
282 268 .AuthUser().permissions['global']
283 269
284 270 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
285 271 extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
286 272
287 273 register_form = RegisterForm(self.request.translate)()
288 274 try:
289 275
290 276 form_result = register_form.to_python(self.request.POST)
291 277 form_result['active'] = auto_active
292 278 external_identity = self.request.POST.get('external_identity')
293 279
294 280 if external_identity:
295 281 extern_name = external_identity
296 282 extern_type = external_identity
297 283
298 284 if captcha.active:
299 285 captcha_status, captcha_message = self.validate_captcha(
300 286 captcha.private_key)
301 287
302 288 if not captcha_status:
303 289 _value = form_result
304 290 _msg = _('Bad captcha')
305 291 error_dict = {'recaptcha_field': captcha_message}
306 292 raise formencode.Invalid(
307 293 _msg, _value, None, error_dict=error_dict)
308 294
309 295 new_user = UserModel().create_registration(
310 296 form_result, extern_name=extern_name, extern_type=extern_type)
311 297
312 298 action_data = {'data': new_user.get_api_data(),
313 299 'user_agent': self.request.user_agent}
314 300
315 301 if external_identity:
316 302 action_data['external_identity'] = external_identity
317 303
318 304 audit_user = audit_logger.UserWrap(
319 305 username=new_user.username,
320 306 user_id=new_user.user_id,
321 307 ip_addr=self.request.remote_addr)
322 308
323 309 audit_logger.store_web(
324 310 'user.register', action_data=action_data,
325 311 user=audit_user)
326 312
327 313 event = UserRegistered(user=new_user, session=self.session)
328 314 trigger(event)
329 315 h.flash(
330 316 _('You have successfully registered with RhodeCode. You can log-in now.'),
331 317 category='success')
332 318 if external_identity:
333 319 h.flash(
334 320 _('Please use the {identity} button to log-in').format(
335 321 identity=external_identity),
336 322 category='success')
337 323 Session().commit()
338 324
339 325 redirect_ro = self.request.route_path('login')
340 326 raise HTTPFound(redirect_ro)
341 327
342 328 except formencode.Invalid as errors:
343 329 errors.value.pop('password', None)
344 330 errors.value.pop('password_confirmation', None)
345 331 return self.register(
346 332 defaults=errors.value, errors=errors.error_dict)
347 333
348 334 except UserCreationError as e:
349 335 # container auth or other auth functions that create users on
350 336 # the fly can throw this exception signaling that there's issue
351 337 # with user creation, explanation should be provided in
352 338 # Exception itself
353 339 h.flash(e, category='error')
354 340 return self.register()
355 341
356 @view_config(
357 route_name='reset_password', request_method=('GET', 'POST'),
358 renderer='rhodecode:templates/password_reset.mako')
359 342 def password_reset(self):
360 343 c = self.load_default_context()
361 344 captcha = self._get_captcha_data()
362 345
363 346 template_context = {
364 347 'captcha_active': captcha.active,
365 348 'captcha_public_key': captcha.public_key,
366 349 'defaults': {},
367 350 'errors': {},
368 351 }
369 352
370 353 # always send implicit message to prevent from discovery of
371 354 # matching emails
372 355 msg = _('If such email exists, a password reset link was sent to it.')
373 356
374 357 def default_response():
375 358 log.debug('faking response on invalid password reset')
376 359 # make this take 2s, to prevent brute forcing.
377 360 time.sleep(2)
378 361 h.flash(msg, category='success')
379 362 return HTTPFound(self.request.route_path('reset_password'))
380 363
381 364 if self.request.POST:
382 365 if h.HasPermissionAny('hg.password_reset.disabled')():
383 366 _email = self.request.POST.get('email', '')
384 367 log.error('Failed attempt to reset password for `%s`.', _email)
385 368 h.flash(_('Password reset has been disabled.'), category='error')
386 369 return HTTPFound(self.request.route_path('reset_password'))
387 370
388 371 password_reset_form = PasswordResetForm(self.request.translate)()
389 372 description = u'Generated token for password reset from {}'.format(
390 373 datetime.datetime.now().isoformat())
391 374
392 375 try:
393 376 form_result = password_reset_form.to_python(
394 377 self.request.POST)
395 378 user_email = form_result['email']
396 379
397 380 if captcha.active:
398 381 captcha_status, captcha_message = self.validate_captcha(
399 382 captcha.private_key)
400 383
401 384 if not captcha_status:
402 385 _value = form_result
403 386 _msg = _('Bad captcha')
404 387 error_dict = {'recaptcha_field': captcha_message}
405 388 raise formencode.Invalid(
406 389 _msg, _value, None, error_dict=error_dict)
407 390
408 391 # Generate reset URL and send mail.
409 392 user = User.get_by_email(user_email)
410 393
411 394 # only allow rhodecode based users to reset their password
412 395 # external auth shouldn't allow password reset
413 396 if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
414 397 log.warning('User %s with external type `%s` tried a password reset. '
415 398 'This try was rejected', user, user.extern_type)
416 399 return default_response()
417 400
418 401 # generate password reset token that expires in 10 minutes
419 402 reset_token = UserModel().add_auth_token(
420 403 user=user, lifetime_minutes=10,
421 404 role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
422 405 description=description)
423 406 Session().commit()
424 407
425 408 log.debug('Successfully created password recovery token')
426 409 password_reset_url = self.request.route_url(
427 410 'reset_password_confirmation',
428 411 _query={'key': reset_token.api_key})
429 412 UserModel().reset_password_link(
430 413 form_result, password_reset_url)
431 414
432 415 action_data = {'email': user_email,
433 416 'user_agent': self.request.user_agent}
434 417 audit_logger.store_web(
435 418 'user.password.reset_request', action_data=action_data,
436 419 user=self._rhodecode_user, commit=True)
437 420
438 421 return default_response()
439 422
440 423 except formencode.Invalid as errors:
441 424 template_context.update({
442 425 'defaults': errors.value,
443 426 'errors': errors.error_dict,
444 427 })
445 428 if not self.request.POST.get('email'):
446 429 # case of empty email, we want to report that
447 430 return self._get_template_context(c, **template_context)
448 431
449 432 if 'recaptcha_field' in errors.error_dict:
450 433 # case of failed captcha
451 434 return self._get_template_context(c, **template_context)
452 435
453 436 return default_response()
454 437
455 438 return self._get_template_context(c, **template_context)
456 439
457 @view_config(route_name='reset_password_confirmation',
458 request_method='GET')
459 440 def password_reset_confirmation(self):
460 441 self.load_default_context()
461 442 if self.request.GET and self.request.GET.get('key'):
462 443 # make this take 2s, to prevent brute forcing.
463 444 time.sleep(2)
464 445
465 446 token = AuthTokenModel().get_auth_token(
466 447 self.request.GET.get('key'))
467 448
468 449 # verify token is the correct role
469 450 if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
470 451 log.debug('Got token with role:%s expected is %s',
471 452 getattr(token, 'role', 'EMPTY_TOKEN'),
472 453 UserApiKeys.ROLE_PASSWORD_RESET)
473 454 h.flash(
474 455 _('Given reset token is invalid'), category='error')
475 456 return HTTPFound(self.request.route_path('reset_password'))
476 457
477 458 try:
478 459 owner = token.user
479 460 data = {'email': owner.email, 'token': token.api_key}
480 461 UserModel().reset_password(data)
481 462 h.flash(
482 463 _('Your password reset was successful, '
483 464 'a new password has been sent to your email'),
484 465 category='success')
485 466 except Exception as e:
486 467 log.error(e)
487 468 return HTTPFound(self.request.route_path('reset_password'))
488 469
489 470 return HTTPFound(self.request.route_path('login'))
@@ -1,160 +1,333 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 def includeme(config):
26 from rhodecode.apps.my_account.views.my_account import MyAccountView
27 from rhodecode.apps.my_account.views.my_account_notifications import MyAccountNotificationsView
28 from rhodecode.apps.my_account.views.my_account_ssh_keys import MyAccountSshKeysView
26 29
27 30 config.add_route(
28 31 name='my_account_profile',
29 32 pattern=ADMIN_PREFIX + '/my_account/profile')
33 config.add_view(
34 MyAccountView,
35 attr='my_account_profile',
36 route_name='my_account_profile', request_method='GET',
37 renderer='rhodecode:templates/admin/my_account/my_account.mako')
30 38
31 39 # my account edit details
32 40 config.add_route(
33 41 name='my_account_edit',
34 42 pattern=ADMIN_PREFIX + '/my_account/edit')
43 config.add_view(
44 MyAccountView,
45 attr='my_account_edit',
46 route_name='my_account_edit',
47 request_method='GET',
48 renderer='rhodecode:templates/admin/my_account/my_account.mako')
49
35 50 config.add_route(
36 51 name='my_account_update',
37 52 pattern=ADMIN_PREFIX + '/my_account/update')
53 config.add_view(
54 MyAccountView,
55 attr='my_account_update',
56 route_name='my_account_update',
57 request_method='POST',
58 renderer='rhodecode:templates/admin/my_account/my_account.mako')
38 59
39 60 # my account password
40 61 config.add_route(
41 62 name='my_account_password',
42 63 pattern=ADMIN_PREFIX + '/my_account/password')
64 config.add_view(
65 MyAccountView,
66 attr='my_account_password',
67 route_name='my_account_password', request_method='GET',
68 renderer='rhodecode:templates/admin/my_account/my_account.mako')
43 69
44 70 config.add_route(
45 71 name='my_account_password_update',
46 72 pattern=ADMIN_PREFIX + '/my_account/password/update')
73 config.add_view(
74 MyAccountView,
75 attr='my_account_password_update',
76 route_name='my_account_password_update', request_method='POST',
77 renderer='rhodecode:templates/admin/my_account/my_account.mako')
47 78
48 79 # my account tokens
49 80 config.add_route(
50 81 name='my_account_auth_tokens',
51 82 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
83 config.add_view(
84 MyAccountView,
85 attr='my_account_auth_tokens',
86 route_name='my_account_auth_tokens', request_method='GET',
87 renderer='rhodecode:templates/admin/my_account/my_account.mako')
88
52 89 config.add_route(
53 90 name='my_account_auth_tokens_view',
54 91 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/view')
92 config.add_view(
93 MyAccountView,
94 attr='my_account_auth_tokens_view',
95 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
96 renderer='json_ext')
97
55 98 config.add_route(
56 99 name='my_account_auth_tokens_add',
57 100 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
101 config.add_view(
102 MyAccountView,
103 attr='my_account_auth_tokens_add',
104 route_name='my_account_auth_tokens_add', request_method='POST')
105
58 106 config.add_route(
59 107 name='my_account_auth_tokens_delete',
60 108 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
109 config.add_view(
110 MyAccountView,
111 attr='my_account_auth_tokens_delete',
112 route_name='my_account_auth_tokens_delete', request_method='POST')
61 113
62 114 # my account ssh keys
63 115 config.add_route(
64 116 name='my_account_ssh_keys',
65 117 pattern=ADMIN_PREFIX + '/my_account/ssh_keys')
118 config.add_view(
119 MyAccountSshKeysView,
120 attr='my_account_ssh_keys',
121 route_name='my_account_ssh_keys', request_method='GET',
122 renderer='rhodecode:templates/admin/my_account/my_account.mako')
123
66 124 config.add_route(
67 125 name='my_account_ssh_keys_generate',
68 126 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate')
127 config.add_view(
128 MyAccountSshKeysView,
129 attr='ssh_keys_generate_keypair',
130 route_name='my_account_ssh_keys_generate', request_method='GET',
131 renderer='rhodecode:templates/admin/my_account/my_account.mako')
132
69 133 config.add_route(
70 134 name='my_account_ssh_keys_add',
71 135 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new')
136 config.add_view(
137 MyAccountSshKeysView,
138 attr='my_account_ssh_keys_add',
139 route_name='my_account_ssh_keys_add', request_method='POST',)
140
72 141 config.add_route(
73 142 name='my_account_ssh_keys_delete',
74 143 pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete')
144 config.add_view(
145 MyAccountSshKeysView,
146 attr='my_account_ssh_keys_delete',
147 route_name='my_account_ssh_keys_delete', request_method='POST')
75 148
76 149 # my account user group membership
77 150 config.add_route(
78 151 name='my_account_user_group_membership',
79 152 pattern=ADMIN_PREFIX + '/my_account/user_group_membership')
153 config.add_view(
154 MyAccountView,
155 attr='my_account_user_group_membership',
156 route_name='my_account_user_group_membership',
157 request_method='GET',
158 renderer='rhodecode:templates/admin/my_account/my_account.mako')
80 159
81 160 # my account emails
82 161 config.add_route(
83 162 name='my_account_emails',
84 163 pattern=ADMIN_PREFIX + '/my_account/emails')
164 config.add_view(
165 MyAccountView,
166 attr='my_account_emails',
167 route_name='my_account_emails', request_method='GET',
168 renderer='rhodecode:templates/admin/my_account/my_account.mako')
169
85 170 config.add_route(
86 171 name='my_account_emails_add',
87 172 pattern=ADMIN_PREFIX + '/my_account/emails/new')
173 config.add_view(
174 MyAccountView,
175 attr='my_account_emails_add',
176 route_name='my_account_emails_add', request_method='POST',
177 renderer='rhodecode:templates/admin/my_account/my_account.mako')
178
88 179 config.add_route(
89 180 name='my_account_emails_delete',
90 181 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
182 config.add_view(
183 MyAccountView,
184 attr='my_account_emails_delete',
185 route_name='my_account_emails_delete', request_method='POST')
91 186
92 187 config.add_route(
93 188 name='my_account_repos',
94 189 pattern=ADMIN_PREFIX + '/my_account/repos')
190 config.add_view(
191 MyAccountView,
192 attr='my_account_repos',
193 route_name='my_account_repos', request_method='GET',
194 renderer='rhodecode:templates/admin/my_account/my_account.mako')
95 195
96 196 config.add_route(
97 197 name='my_account_watched',
98 198 pattern=ADMIN_PREFIX + '/my_account/watched')
199 config.add_view(
200 MyAccountView,
201 attr='my_account_watched',
202 route_name='my_account_watched', request_method='GET',
203 renderer='rhodecode:templates/admin/my_account/my_account.mako')
99 204
100 205 config.add_route(
101 206 name='my_account_bookmarks',
102 207 pattern=ADMIN_PREFIX + '/my_account/bookmarks')
208 config.add_view(
209 MyAccountView,
210 attr='my_account_bookmarks',
211 route_name='my_account_bookmarks', request_method='GET',
212 renderer='rhodecode:templates/admin/my_account/my_account.mako')
103 213
104 214 config.add_route(
105 215 name='my_account_bookmarks_update',
106 216 pattern=ADMIN_PREFIX + '/my_account/bookmarks/update')
217 config.add_view(
218 MyAccountView,
219 attr='my_account_bookmarks_update',
220 route_name='my_account_bookmarks_update', request_method='POST')
107 221
108 222 config.add_route(
109 223 name='my_account_goto_bookmark',
110 224 pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}')
225 config.add_view(
226 MyAccountView,
227 attr='my_account_goto_bookmark',
228 route_name='my_account_goto_bookmark', request_method='GET',
229 renderer='rhodecode:templates/admin/my_account/my_account.mako')
111 230
112 231 config.add_route(
113 232 name='my_account_perms',
114 233 pattern=ADMIN_PREFIX + '/my_account/perms')
234 config.add_view(
235 MyAccountView,
236 attr='my_account_perms',
237 route_name='my_account_perms', request_method='GET',
238 renderer='rhodecode:templates/admin/my_account/my_account.mako')
115 239
116 240 config.add_route(
117 241 name='my_account_notifications',
118 242 pattern=ADMIN_PREFIX + '/my_account/notifications')
243 config.add_view(
244 MyAccountView,
245 attr='my_notifications',
246 route_name='my_account_notifications', request_method='GET',
247 renderer='rhodecode:templates/admin/my_account/my_account.mako')
119 248
120 249 config.add_route(
121 250 name='my_account_notifications_toggle_visibility',
122 251 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
252 config.add_view(
253 MyAccountView,
254 attr='my_notifications_toggle_visibility',
255 route_name='my_account_notifications_toggle_visibility',
256 request_method='POST', renderer='json_ext')
123 257
124 258 # my account pull requests
125 259 config.add_route(
126 260 name='my_account_pullrequests',
127 261 pattern=ADMIN_PREFIX + '/my_account/pull_requests')
262 config.add_view(
263 MyAccountView,
264 attr='my_account_pullrequests',
265 route_name='my_account_pullrequests',
266 request_method='GET',
267 renderer='rhodecode:templates/admin/my_account/my_account.mako')
268
128 269 config.add_route(
129 270 name='my_account_pullrequests_data',
130 271 pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
272 config.add_view(
273 MyAccountView,
274 attr='my_account_pullrequests_data',
275 route_name='my_account_pullrequests_data',
276 request_method='GET', renderer='json_ext')
277
278 # channelstream test
279 config.add_route(
280 name='my_account_notifications_test_channelstream',
281 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
282 config.add_view(
283 MyAccountView,
284 attr='my_account_notifications_test_channelstream',
285 route_name='my_account_notifications_test_channelstream',
286 request_method='POST', renderer='json_ext')
131 287
132 288 # notifications
133 289 config.add_route(
134 290 name='notifications_show_all',
135 291 pattern=ADMIN_PREFIX + '/notifications')
292 config.add_view(
293 MyAccountNotificationsView,
294 attr='notifications_show_all',
295 route_name='notifications_show_all', request_method='GET',
296 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
136 297
137 298 # notifications
138 299 config.add_route(
139 300 name='notifications_mark_all_read',
140 pattern=ADMIN_PREFIX + '/notifications/mark_all_read')
301 pattern=ADMIN_PREFIX + '/notifications_mark_all_read')
302 config.add_view(
303 MyAccountNotificationsView,
304 attr='notifications_mark_all_read',
305 route_name='notifications_mark_all_read', request_method='POST',
306 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
141 307
142 308 config.add_route(
143 309 name='notifications_show',
144 310 pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
311 config.add_view(
312 MyAccountNotificationsView,
313 attr='notifications_show',
314 route_name='notifications_show', request_method='GET',
315 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
145 316
146 317 config.add_route(
147 318 name='notifications_update',
148 319 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
320 config.add_view(
321 MyAccountNotificationsView,
322 attr='notification_update',
323 route_name='notifications_update', request_method='POST',
324 renderer='json_ext')
149 325
150 326 config.add_route(
151 327 name='notifications_delete',
152 328 pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete')
153
154 # channelstream test
155 config.add_route(
156 name='my_account_notifications_test_channelstream',
157 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
158
159 # Scan module for configuration decorators.
160 config.scan('.views', ignore='.tests')
329 config.add_view(
330 MyAccountNotificationsView,
331 attr='notification_delete',
332 route_name='notifications_delete', request_method='POST',
333 renderer='json_ext')
@@ -1,197 +1,206 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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 pytest
22 22
23 23 from rhodecode.apps._base import ADMIN_PREFIX
24 24 from rhodecode.tests import (
25 25 TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
26 26 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
27 27 from rhodecode.tests.fixture import Fixture
28 28
29 29 from rhodecode.model.db import Notification, User
30 30 from rhodecode.model.user import UserModel
31 31 from rhodecode.model.notification import NotificationModel
32 32 from rhodecode.model.meta import Session
33 33
34 34 fixture = Fixture()
35 35
36 36
37 37 def route_path(name, params=None, **kwargs):
38 38 import urllib
39 39 from rhodecode.apps._base import ADMIN_PREFIX
40 40
41 41 base_url = {
42 42 'notifications_show_all': ADMIN_PREFIX + '/notifications',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications/mark_all_read',
43 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications_mark_all_read',
44 44 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
45 45 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
46 46 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
47 47
48 48 }[name].format(**kwargs)
49 49
50 50 if params:
51 51 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
52 52 return base_url
53 53
54 54
55 55 class TestNotificationsController(TestController):
56 56
57 57 def teardown_method(self, method):
58 58 for n in Notification.query().all():
59 59 inst = Notification.get(n.notification_id)
60 60 Session().delete(inst)
61 61 Session().commit()
62 62
63 def test_mark_all_read(self, user_util):
64 user = user_util.create_user(password='qweqwe')
65 self.log_user(user.username, 'qweqwe')
66
67 self.app.post(
68 route_path('notifications_mark_all_read'), status=302,
69 params={'csrf_token': self.csrf_token}
70 )
71
63 72 def test_show_all(self, user_util):
64 73 user = user_util.create_user(password='qweqwe')
65 74 user_id = user.user_id
66 75 self.log_user(user.username, 'qweqwe')
67 76
68 77 response = self.app.get(
69 78 route_path('notifications_show_all', params={'type': 'all'}))
70 79 response.mustcontain(
71 80 '<div class="table">No notifications here yet</div>')
72 81
73 82 notification = NotificationModel().create(
74 83 created_by=user_id, notification_subject=u'test_notification_1',
75 84 notification_body=u'notification_1', recipients=[user_id])
76 85 Session().commit()
77 86 notification_id = notification.notification_id
78 87
79 88 response = self.app.get(route_path('notifications_show_all',
80 89 params={'type': 'all'}))
81 90 response.mustcontain('id="notification_%s"' % notification_id)
82 91
83 92 def test_show_unread(self, user_util):
84 93 user = user_util.create_user(password='qweqwe')
85 94 user_id = user.user_id
86 95 self.log_user(user.username, 'qweqwe')
87 96
88 97 response = self.app.get(route_path('notifications_show_all'))
89 98 response.mustcontain(
90 99 '<div class="table">No notifications here yet</div>')
91 100
92 101 notification = NotificationModel().create(
93 102 created_by=user_id, notification_subject=u'test_notification_1',
94 103 notification_body=u'notification_1', recipients=[user_id])
95 104
96 105 # mark the USER notification as unread
97 106 user_notification = NotificationModel().get_user_notification(
98 107 user_id, notification)
99 108 user_notification.read = False
100 109
101 110 Session().commit()
102 111 notification_id = notification.notification_id
103 112
104 113 response = self.app.get(route_path('notifications_show_all'))
105 114 response.mustcontain('id="notification_%s"' % notification_id)
106 115 response.mustcontain('<div class="desc unread')
107 116
108 117 @pytest.mark.parametrize('user,password', [
109 118 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
110 119 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
111 120 ])
112 121 def test_delete(self, user, password, user_util):
113 122 self.log_user(user, password)
114 123 cur_user = self._get_logged_user()
115 124
116 125 u1 = user_util.create_user()
117 126 u2 = user_util.create_user()
118 127
119 128 # make notifications
120 129 notification = NotificationModel().create(
121 130 created_by=cur_user, notification_subject=u'test',
122 131 notification_body=u'hi there', recipients=[cur_user, u1, u2])
123 132 Session().commit()
124 133 u1 = User.get(u1.user_id)
125 134 u2 = User.get(u2.user_id)
126 135
127 136 # check DB
128 137 get_notif = lambda un: [x.notification for x in un]
129 138 assert get_notif(cur_user.notifications) == [notification]
130 139 assert get_notif(u1.notifications) == [notification]
131 140 assert get_notif(u2.notifications) == [notification]
132 141 cur_usr_id = cur_user.user_id
133 142
134 143 response = self.app.post(
135 144 route_path('notifications_delete',
136 145 notification_id=notification.notification_id),
137 146 params={'csrf_token': self.csrf_token})
138 147 assert response.json == 'ok'
139 148
140 149 cur_user = User.get(cur_usr_id)
141 150 assert cur_user.notifications == []
142 151
143 152 @pytest.mark.parametrize('user,password', [
144 153 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
145 154 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
146 155 ])
147 156 def test_show(self, user, password, user_util):
148 157 self.log_user(user, password)
149 158 cur_user = self._get_logged_user()
150 159 u1 = user_util.create_user()
151 160 u2 = user_util.create_user()
152 161
153 162 subject = u'test'
154 163 notif_body = u'hi there'
155 164 notification = NotificationModel().create(
156 165 created_by=cur_user, notification_subject=subject,
157 166 notification_body=notif_body, recipients=[cur_user, u1, u2])
158 167 Session().commit()
159 168
160 169 response = self.app.get(
161 170 route_path('notifications_show',
162 171 notification_id=notification.notification_id))
163 172
164 173 response.mustcontain(subject)
165 174 response.mustcontain(notif_body)
166 175
167 176 @pytest.mark.parametrize('user,password', [
168 177 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
169 178 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
170 179 ])
171 180 def test_update(self, user, password, user_util):
172 181 self.log_user(user, password)
173 182 cur_user = self._get_logged_user()
174 183 u1 = user_util.create_user()
175 184 u2 = user_util.create_user()
176 185
177 186 # make notifications
178 187 recipients = [cur_user, u1, u2]
179 188 notification = NotificationModel().create(
180 189 created_by=cur_user, notification_subject=u'test',
181 190 notification_body=u'hi there', recipients=recipients)
182 191 Session().commit()
183 192
184 193 for u_obj in recipients:
185 194 # if it's current user, he has his message already read
186 195 read = u_obj.username == user
187 196 assert len(u_obj.notifications) == 1
188 197 assert u_obj.notifications[0].read == read
189 198
190 199 response = self.app.post(
191 200 route_path('notifications_update',
192 201 notification_id=notification.notification_id),
193 202 params={'csrf_token': self.csrf_token})
194 203 assert response.json == 'ok'
195 204
196 205 cur_user = self._get_logged_user()
197 206 assert True is cur_user.notifications[0].read
@@ -1,826 +1,752 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 datetime
23 23 import string
24 24
25 25 import formencode
26 26 import formencode.htmlfill
27 27 import peppercorn
28 28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29 from pyramid.view import view_config
30 29
31 30 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 31 from rhodecode import forms
33 32 from rhodecode.lib import helpers as h
34 33 from rhodecode.lib import audit_logger
35 34 from rhodecode.lib.ext_json import json
36 35 from rhodecode.lib.auth import (
37 36 LoginRequired, NotAnonymous, CSRFRequired,
38 37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
39 38 from rhodecode.lib.channelstream import (
40 39 channelstream_request, ChannelstreamException)
41 40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
42 41 from rhodecode.model.auth_token import AuthTokenModel
43 42 from rhodecode.model.comment import CommentsModel
44 43 from rhodecode.model.db import (
45 44 IntegrityError, or_, in_filter_generator,
46 45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
47 46 PullRequest, UserBookmark, RepoGroup)
48 47 from rhodecode.model.meta import Session
49 48 from rhodecode.model.pull_request import PullRequestModel
50 49 from rhodecode.model.user import UserModel
51 50 from rhodecode.model.user_group import UserGroupModel
52 51 from rhodecode.model.validation_schema.schemas import user_schema
53 52
54 53 log = logging.getLogger(__name__)
55 54
56 55
57 56 class MyAccountView(BaseAppView, DataGridAppView):
58 57 ALLOW_SCOPED_TOKENS = False
59 58 """
60 59 This view has alternative version inside EE, if modified please take a look
61 60 in there as well.
62 61 """
63 62
64 63 def load_default_context(self):
65 64 c = self._get_local_tmpl_context()
66 65 c.user = c.auth_user.get_instance()
67 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
68
69 67 return c
70 68
71 69 @LoginRequired()
72 70 @NotAnonymous()
73 @view_config(
74 route_name='my_account_profile', request_method='GET',
75 renderer='rhodecode:templates/admin/my_account/my_account.mako')
76 71 def my_account_profile(self):
77 72 c = self.load_default_context()
78 73 c.active = 'profile'
79 74 c.extern_type = c.user.extern_type
80 75 return self._get_template_context(c)
81 76
82 77 @LoginRequired()
83 78 @NotAnonymous()
84 @view_config(
85 route_name='my_account_password', request_method='GET',
86 renderer='rhodecode:templates/admin/my_account/my_account.mako')
79 def my_account_edit(self):
80 c = self.load_default_context()
81 c.active = 'profile_edit'
82 c.extern_type = c.user.extern_type
83 c.extern_name = c.user.extern_name
84
85 schema = user_schema.UserProfileSchema().bind(
86 username=c.user.username, user_emails=c.user.emails)
87 appstruct = {
88 'username': c.user.username,
89 'email': c.user.email,
90 'firstname': c.user.firstname,
91 'lastname': c.user.lastname,
92 'description': c.user.description,
93 }
94 c.form = forms.RcForm(
95 schema, appstruct=appstruct,
96 action=h.route_path('my_account_update'),
97 buttons=(forms.buttons.save, forms.buttons.reset))
98
99 return self._get_template_context(c)
100
101 @LoginRequired()
102 @NotAnonymous()
103 @CSRFRequired()
104 def my_account_update(self):
105 _ = self.request.translate
106 c = self.load_default_context()
107 c.active = 'profile_edit'
108 c.perm_user = c.auth_user
109 c.extern_type = c.user.extern_type
110 c.extern_name = c.user.extern_name
111
112 schema = user_schema.UserProfileSchema().bind(
113 username=c.user.username, user_emails=c.user.emails)
114 form = forms.RcForm(
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116
117 controls = self.request.POST.items()
118 try:
119 valid_data = form.validate(controls)
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 'new_password', 'password_confirmation']
122 if c.extern_type != "rhodecode":
123 # forbid updating username for external accounts
124 skip_attrs.append('username')
125 old_email = c.user.email
126 UserModel().update_user(
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 **valid_data)
129 if old_email != valid_data['email']:
130 old = UserEmailMap.query() \
131 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.email == valid_data['email'])\
133 .first()
134 old.email = old_email
135 h.flash(_('Your account was updated successfully'), category='success')
136 Session().commit()
137 except forms.ValidationFailure as e:
138 c.form = e
139 return self._get_template_context(c)
140 except Exception:
141 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
143 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
145
146 @LoginRequired()
147 @NotAnonymous()
87 148 def my_account_password(self):
88 149 c = self.load_default_context()
89 150 c.active = 'password'
90 151 c.extern_type = c.user.extern_type
91 152
92 153 schema = user_schema.ChangePasswordSchema().bind(
93 154 username=c.user.username)
94 155
95 156 form = forms.Form(
96 157 schema,
97 158 action=h.route_path('my_account_password_update'),
98 159 buttons=(forms.buttons.save, forms.buttons.reset))
99 160
100 161 c.form = form
101 162 return self._get_template_context(c)
102 163
103 164 @LoginRequired()
104 165 @NotAnonymous()
105 166 @CSRFRequired()
106 @view_config(
107 route_name='my_account_password_update', request_method='POST',
108 renderer='rhodecode:templates/admin/my_account/my_account.mako')
109 167 def my_account_password_update(self):
110 168 _ = self.request.translate
111 169 c = self.load_default_context()
112 170 c.active = 'password'
113 171 c.extern_type = c.user.extern_type
114 172
115 173 schema = user_schema.ChangePasswordSchema().bind(
116 174 username=c.user.username)
117 175
118 176 form = forms.Form(
119 177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
120 178
121 179 if c.extern_type != 'rhodecode':
122 180 raise HTTPFound(self.request.route_path('my_account_password'))
123 181
124 182 controls = self.request.POST.items()
125 183 try:
126 184 valid_data = form.validate(controls)
127 185 UserModel().update_user(c.user.user_id, **valid_data)
128 186 c.user.update_userdata(force_password_change=False)
129 187 Session().commit()
130 188 except forms.ValidationFailure as e:
131 189 c.form = e
132 190 return self._get_template_context(c)
133 191
134 192 except Exception:
135 193 log.exception("Exception updating password")
136 194 h.flash(_('Error occurred during update of user password'),
137 195 category='error')
138 196 else:
139 197 instance = c.auth_user.get_instance()
140 198 self.session.setdefault('rhodecode_user', {}).update(
141 199 {'password': md5(instance.password)})
142 200 self.session.save()
143 201 h.flash(_("Successfully updated password"), category='success')
144 202
145 203 raise HTTPFound(self.request.route_path('my_account_password'))
146 204
147 205 @LoginRequired()
148 206 @NotAnonymous()
149 @view_config(
150 route_name='my_account_auth_tokens', request_method='GET',
151 renderer='rhodecode:templates/admin/my_account/my_account.mako')
152 207 def my_account_auth_tokens(self):
153 208 _ = self.request.translate
154 209
155 210 c = self.load_default_context()
156 211 c.active = 'auth_tokens'
157 212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
158 213 c.role_values = [
159 214 (x, AuthTokenModel.cls._get_role_name(x))
160 215 for x in AuthTokenModel.cls.ROLES]
161 216 c.role_options = [(c.role_values, _("Role"))]
162 217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
163 218 c.user.user_id, show_expired=True)
164 219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
165 220 return self._get_template_context(c)
166 221
167 222 @LoginRequired()
168 223 @NotAnonymous()
169 224 @CSRFRequired()
170 @view_config(
171 route_name='my_account_auth_tokens_view', request_method='POST', xhr=True,
172 renderer='json_ext')
173 225 def my_account_auth_tokens_view(self):
174 226 _ = self.request.translate
175 227 c = self.load_default_context()
176 228
177 229 auth_token_id = self.request.POST.get('auth_token_id')
178 230
179 231 if auth_token_id:
180 232 token = UserApiKeys.get_or_404(auth_token_id)
181 233 if token.user.user_id != c.user.user_id:
182 234 raise HTTPNotFound()
183 235
184 236 return {
185 237 'auth_token': token.api_key
186 238 }
187 239
188 240 def maybe_attach_token_scope(self, token):
189 241 # implemented in EE edition
190 242 pass
191 243
192 244 @LoginRequired()
193 245 @NotAnonymous()
194 246 @CSRFRequired()
195 @view_config(
196 route_name='my_account_auth_tokens_add', request_method='POST',)
197 247 def my_account_auth_tokens_add(self):
198 248 _ = self.request.translate
199 249 c = self.load_default_context()
200 250
201 251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 252 description = self.request.POST.get('description')
203 253 role = self.request.POST.get('role')
204 254
205 255 token = UserModel().add_auth_token(
206 256 user=c.user.user_id,
207 257 lifetime_minutes=lifetime, role=role, description=description,
208 258 scope_callback=self.maybe_attach_token_scope)
209 259 token_data = token.get_api_data()
210 260
211 261 audit_logger.store_web(
212 262 'user.edit.token.add', action_data={
213 263 'data': {'token': token_data, 'user': 'self'}},
214 264 user=self._rhodecode_user, )
215 265 Session().commit()
216 266
217 267 h.flash(_("Auth token successfully created"), category='success')
218 268 return HTTPFound(h.route_path('my_account_auth_tokens'))
219 269
220 270 @LoginRequired()
221 271 @NotAnonymous()
222 272 @CSRFRequired()
223 @view_config(
224 route_name='my_account_auth_tokens_delete', request_method='POST')
225 273 def my_account_auth_tokens_delete(self):
226 274 _ = self.request.translate
227 275 c = self.load_default_context()
228 276
229 277 del_auth_token = self.request.POST.get('del_auth_token')
230 278
231 279 if del_auth_token:
232 280 token = UserApiKeys.get_or_404(del_auth_token)
233 281 token_data = token.get_api_data()
234 282
235 283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
236 284 audit_logger.store_web(
237 285 'user.edit.token.delete', action_data={
238 286 'data': {'token': token_data, 'user': 'self'}},
239 287 user=self._rhodecode_user,)
240 288 Session().commit()
241 289 h.flash(_("Auth token successfully deleted"), category='success')
242 290
243 291 return HTTPFound(h.route_path('my_account_auth_tokens'))
244 292
245 293 @LoginRequired()
246 294 @NotAnonymous()
247 @view_config(
248 route_name='my_account_emails', request_method='GET',
249 renderer='rhodecode:templates/admin/my_account/my_account.mako')
250 295 def my_account_emails(self):
251 296 _ = self.request.translate
252 297
253 298 c = self.load_default_context()
254 299 c.active = 'emails'
255 300
256 301 c.user_email_map = UserEmailMap.query()\
257 302 .filter(UserEmailMap.user == c.user).all()
258 303
259 304 schema = user_schema.AddEmailSchema().bind(
260 305 username=c.user.username, user_emails=c.user.emails)
261 306
262 307 form = forms.RcForm(schema,
263 308 action=h.route_path('my_account_emails_add'),
264 309 buttons=(forms.buttons.save, forms.buttons.reset))
265 310
266 311 c.form = form
267 312 return self._get_template_context(c)
268 313
269 314 @LoginRequired()
270 315 @NotAnonymous()
271 316 @CSRFRequired()
272 @view_config(
273 route_name='my_account_emails_add', request_method='POST',
274 renderer='rhodecode:templates/admin/my_account/my_account.mako')
275 317 def my_account_emails_add(self):
276 318 _ = self.request.translate
277 319 c = self.load_default_context()
278 320 c.active = 'emails'
279 321
280 322 schema = user_schema.AddEmailSchema().bind(
281 323 username=c.user.username, user_emails=c.user.emails)
282 324
283 325 form = forms.RcForm(
284 326 schema, action=h.route_path('my_account_emails_add'),
285 327 buttons=(forms.buttons.save, forms.buttons.reset))
286 328
287 329 controls = self.request.POST.items()
288 330 try:
289 331 valid_data = form.validate(controls)
290 332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
291 333 audit_logger.store_web(
292 334 'user.edit.email.add', action_data={
293 335 'data': {'email': valid_data['email'], 'user': 'self'}},
294 336 user=self._rhodecode_user,)
295 337 Session().commit()
296 338 except formencode.Invalid as error:
297 339 h.flash(h.escape(error.error_dict['email']), category='error')
298 340 except forms.ValidationFailure as e:
299 341 c.user_email_map = UserEmailMap.query() \
300 342 .filter(UserEmailMap.user == c.user).all()
301 343 c.form = e
302 344 return self._get_template_context(c)
303 345 except Exception:
304 346 log.exception("Exception adding email")
305 347 h.flash(_('Error occurred during adding email'),
306 348 category='error')
307 349 else:
308 350 h.flash(_("Successfully added email"), category='success')
309 351
310 352 raise HTTPFound(self.request.route_path('my_account_emails'))
311 353
312 354 @LoginRequired()
313 355 @NotAnonymous()
314 356 @CSRFRequired()
315 @view_config(
316 route_name='my_account_emails_delete', request_method='POST')
317 357 def my_account_emails_delete(self):
318 358 _ = self.request.translate
319 359 c = self.load_default_context()
320 360
321 361 del_email_id = self.request.POST.get('del_email_id')
322 362 if del_email_id:
323 363 email = UserEmailMap.get_or_404(del_email_id).email
324 364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
325 365 audit_logger.store_web(
326 366 'user.edit.email.delete', action_data={
327 367 'data': {'email': email, 'user': 'self'}},
328 368 user=self._rhodecode_user,)
329 369 Session().commit()
330 370 h.flash(_("Email successfully deleted"),
331 371 category='success')
332 372 return HTTPFound(h.route_path('my_account_emails'))
333 373
334 374 @LoginRequired()
335 375 @NotAnonymous()
336 376 @CSRFRequired()
337 @view_config(
338 route_name='my_account_notifications_test_channelstream',
339 request_method='POST', renderer='json_ext')
340 377 def my_account_notifications_test_channelstream(self):
341 378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
342 379 self._rhodecode_user.username, datetime.datetime.now())
343 380 payload = {
344 381 # 'channel': 'broadcast',
345 382 'type': 'message',
346 383 'timestamp': datetime.datetime.utcnow(),
347 384 'user': 'system',
348 385 'pm_users': [self._rhodecode_user.username],
349 386 'message': {
350 387 'message': message,
351 388 'level': 'info',
352 389 'topic': '/notifications'
353 390 }
354 391 }
355 392
356 393 registry = self.request.registry
357 394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 395 channelstream_config = rhodecode_plugins.get('channelstream', {})
359 396
360 397 try:
361 398 channelstream_request(channelstream_config, [payload], '/message')
362 399 except ChannelstreamException as e:
363 400 log.exception('Failed to send channelstream data')
364 401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
365 402 return {"response": 'Channelstream data sent. '
366 403 'You should see a new live message now.'}
367 404
368 405 def _load_my_repos_data(self, watched=False):
369 406
370 407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
371 408
372 409 if watched:
373 410 # repos user watch
374 411 repo_list = Session().query(
375 412 Repository
376 413 ) \
377 414 .join(
378 415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
379 416 ) \
380 417 .filter(
381 418 UserFollowing.user_id == self._rhodecode_user.user_id
382 419 ) \
383 420 .filter(or_(
384 421 # generate multiple IN to fix limitation problems
385 422 *in_filter_generator(Repository.repo_id, allowed_ids))
386 423 ) \
387 424 .order_by(Repository.repo_name) \
388 425 .all()
389 426
390 427 else:
391 428 # repos user is owner of
392 429 repo_list = Session().query(
393 430 Repository
394 431 ) \
395 432 .filter(
396 433 Repository.user_id == self._rhodecode_user.user_id
397 434 ) \
398 435 .filter(or_(
399 436 # generate multiple IN to fix limitation problems
400 437 *in_filter_generator(Repository.repo_id, allowed_ids))
401 438 ) \
402 439 .order_by(Repository.repo_name) \
403 440 .all()
404 441
405 442 _render = self.request.get_partial_renderer(
406 443 'rhodecode:templates/data_table/_dt_elements.mako')
407 444
408 445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
409 446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
410 447 short_name=False, admin=False)
411 448
412 449 repos_data = []
413 450 for repo in repo_list:
414 451 row = {
415 452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
416 453 repo.private, repo.archived, repo.fork),
417 454 "name_raw": repo.repo_name.lower(),
418 455 }
419 456
420 457 repos_data.append(row)
421 458
422 459 # json used to render the grid
423 460 return json.dumps(repos_data)
424 461
425 462 @LoginRequired()
426 463 @NotAnonymous()
427 @view_config(
428 route_name='my_account_repos', request_method='GET',
429 renderer='rhodecode:templates/admin/my_account/my_account.mako')
430 464 def my_account_repos(self):
431 465 c = self.load_default_context()
432 466 c.active = 'repos'
433 467
434 468 # json used to render the grid
435 469 c.data = self._load_my_repos_data()
436 470 return self._get_template_context(c)
437 471
438 472 @LoginRequired()
439 473 @NotAnonymous()
440 @view_config(
441 route_name='my_account_watched', request_method='GET',
442 renderer='rhodecode:templates/admin/my_account/my_account.mako')
443 474 def my_account_watched(self):
444 475 c = self.load_default_context()
445 476 c.active = 'watched'
446 477
447 478 # json used to render the grid
448 479 c.data = self._load_my_repos_data(watched=True)
449 480 return self._get_template_context(c)
450 481
451 482 @LoginRequired()
452 483 @NotAnonymous()
453 @view_config(
454 route_name='my_account_bookmarks', request_method='GET',
455 renderer='rhodecode:templates/admin/my_account/my_account.mako')
456 484 def my_account_bookmarks(self):
457 485 c = self.load_default_context()
458 486 c.active = 'bookmarks'
459 487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
460 488 self._rhodecode_db_user.user_id, cache=False)
461 489 return self._get_template_context(c)
462 490
463 491 def _process_bookmark_entry(self, entry, user_id):
464 492 position = safe_int(entry.get('position'))
465 493 cur_position = safe_int(entry.get('cur_position'))
466 494 if position is None:
467 495 return
468 496
469 497 # check if this is an existing entry
470 498 is_new = False
471 499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
472 500
473 501 if db_entry and str2bool(entry.get('remove')):
474 502 log.debug('Marked bookmark %s for deletion', db_entry)
475 503 Session().delete(db_entry)
476 504 return
477 505
478 506 if not db_entry:
479 507 # new
480 508 db_entry = UserBookmark()
481 509 is_new = True
482 510
483 511 should_save = False
484 512 default_redirect_url = ''
485 513
486 514 # save repo
487 515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
488 516 repo = Repository.get(entry['bookmark_repo'])
489 517 perm_check = HasRepoPermissionAny(
490 518 'repository.read', 'repository.write', 'repository.admin')
491 519 if repo and perm_check(repo_name=repo.repo_name):
492 520 db_entry.repository = repo
493 521 should_save = True
494 522 default_redirect_url = '${repo_url}'
495 523 # save repo group
496 524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
497 525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
498 526 perm_check = HasRepoGroupPermissionAny(
499 527 'group.read', 'group.write', 'group.admin')
500 528
501 529 if repo_group and perm_check(group_name=repo_group.group_name):
502 530 db_entry.repository_group = repo_group
503 531 should_save = True
504 532 default_redirect_url = '${repo_group_url}'
505 533 # save generic info
506 534 elif entry.get('title') and entry.get('redirect_url'):
507 535 should_save = True
508 536
509 537 if should_save:
510 538 # mark user and position
511 539 db_entry.user_id = user_id
512 540 db_entry.position = position
513 541 db_entry.title = entry.get('title')
514 542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
515 543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
516 544
517 545 Session().add(db_entry)
518 546
519 547 @LoginRequired()
520 548 @NotAnonymous()
521 549 @CSRFRequired()
522 @view_config(
523 route_name='my_account_bookmarks_update', request_method='POST')
524 550 def my_account_bookmarks_update(self):
525 551 _ = self.request.translate
526 552 c = self.load_default_context()
527 553 c.active = 'bookmarks'
528 554
529 555 controls = peppercorn.parse(self.request.POST.items())
530 556 user_id = c.user.user_id
531 557
532 558 # validate positions
533 559 positions = {}
534 560 for entry in controls.get('bookmarks', []):
535 561 position = safe_int(entry['position'])
536 562 if position is None:
537 563 continue
538 564
539 565 if position in positions:
540 566 h.flash(_("Position {} is defined twice. "
541 567 "Please correct this error.").format(position), category='error')
542 568 return HTTPFound(h.route_path('my_account_bookmarks'))
543 569
544 570 entry['position'] = position
545 571 entry['cur_position'] = safe_int(entry.get('cur_position'))
546 572 positions[position] = entry
547 573
548 574 try:
549 575 for entry in positions.values():
550 576 self._process_bookmark_entry(entry, user_id)
551 577
552 578 Session().commit()
553 579 h.flash(_("Update Bookmarks"), category='success')
554 580 except IntegrityError:
555 581 h.flash(_("Failed to update bookmarks. "
556 582 "Make sure an unique position is used."), category='error')
557 583
558 584 return HTTPFound(h.route_path('my_account_bookmarks'))
559 585
560 586 @LoginRequired()
561 587 @NotAnonymous()
562 @view_config(
563 route_name='my_account_goto_bookmark', request_method='GET',
564 renderer='rhodecode:templates/admin/my_account/my_account.mako')
565 588 def my_account_goto_bookmark(self):
566 589
567 590 bookmark_id = self.request.matchdict['bookmark_id']
568 591 user_bookmark = UserBookmark().query()\
569 592 .filter(UserBookmark.user_id == self.request.user.user_id) \
570 593 .filter(UserBookmark.position == bookmark_id).scalar()
571 594
572 595 redirect_url = h.route_path('my_account_bookmarks')
573 596 if not user_bookmark:
574 597 raise HTTPFound(redirect_url)
575 598
576 599 # repository set
577 600 if user_bookmark.repository:
578 601 repo_name = user_bookmark.repository.repo_name
579 602 base_redirect_url = h.route_path(
580 603 'repo_summary', repo_name=repo_name)
581 604 if user_bookmark.redirect_url and \
582 605 '${repo_url}' in user_bookmark.redirect_url:
583 606 redirect_url = string.Template(user_bookmark.redirect_url)\
584 607 .safe_substitute({'repo_url': base_redirect_url})
585 608 else:
586 609 redirect_url = base_redirect_url
587 610 # repository group set
588 611 elif user_bookmark.repository_group:
589 612 repo_group_name = user_bookmark.repository_group.group_name
590 613 base_redirect_url = h.route_path(
591 614 'repo_group_home', repo_group_name=repo_group_name)
592 615 if user_bookmark.redirect_url and \
593 616 '${repo_group_url}' in user_bookmark.redirect_url:
594 617 redirect_url = string.Template(user_bookmark.redirect_url)\
595 618 .safe_substitute({'repo_group_url': base_redirect_url})
596 619 else:
597 620 redirect_url = base_redirect_url
598 621 # custom URL set
599 622 elif user_bookmark.redirect_url:
600 623 server_url = h.route_url('home').rstrip('/')
601 624 redirect_url = string.Template(user_bookmark.redirect_url) \
602 625 .safe_substitute({'server_url': server_url})
603 626
604 627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
605 628 raise HTTPFound(redirect_url)
606 629
607 630 @LoginRequired()
608 631 @NotAnonymous()
609 @view_config(
610 route_name='my_account_perms', request_method='GET',
611 renderer='rhodecode:templates/admin/my_account/my_account.mako')
612 632 def my_account_perms(self):
613 633 c = self.load_default_context()
614 634 c.active = 'perms'
615 635
616 636 c.perm_user = c.auth_user
617 637 return self._get_template_context(c)
618 638
619 639 @LoginRequired()
620 640 @NotAnonymous()
621 @view_config(
622 route_name='my_account_notifications', request_method='GET',
623 renderer='rhodecode:templates/admin/my_account/my_account.mako')
624 641 def my_notifications(self):
625 642 c = self.load_default_context()
626 643 c.active = 'notifications'
627 644
628 645 return self._get_template_context(c)
629 646
630 647 @LoginRequired()
631 648 @NotAnonymous()
632 649 @CSRFRequired()
633 @view_config(
634 route_name='my_account_notifications_toggle_visibility',
635 request_method='POST', renderer='json_ext')
636 650 def my_notifications_toggle_visibility(self):
637 651 user = self._rhodecode_db_user
638 652 new_status = not user.user_data.get('notification_status', True)
639 653 user.update_userdata(notification_status=new_status)
640 654 Session().commit()
641 655 return user.user_data['notification_status']
642 656
643 @LoginRequired()
644 @NotAnonymous()
645 @view_config(
646 route_name='my_account_edit',
647 request_method='GET',
648 renderer='rhodecode:templates/admin/my_account/my_account.mako')
649 def my_account_edit(self):
650 c = self.load_default_context()
651 c.active = 'profile_edit'
652 c.extern_type = c.user.extern_type
653 c.extern_name = c.user.extern_name
654
655 schema = user_schema.UserProfileSchema().bind(
656 username=c.user.username, user_emails=c.user.emails)
657 appstruct = {
658 'username': c.user.username,
659 'email': c.user.email,
660 'firstname': c.user.firstname,
661 'lastname': c.user.lastname,
662 'description': c.user.description,
663 }
664 c.form = forms.RcForm(
665 schema, appstruct=appstruct,
666 action=h.route_path('my_account_update'),
667 buttons=(forms.buttons.save, forms.buttons.reset))
668
669 return self._get_template_context(c)
670
671 @LoginRequired()
672 @NotAnonymous()
673 @CSRFRequired()
674 @view_config(
675 route_name='my_account_update',
676 request_method='POST',
677 renderer='rhodecode:templates/admin/my_account/my_account.mako')
678 def my_account_update(self):
679 _ = self.request.translate
680 c = self.load_default_context()
681 c.active = 'profile_edit'
682 c.perm_user = c.auth_user
683 c.extern_type = c.user.extern_type
684 c.extern_name = c.user.extern_name
685
686 schema = user_schema.UserProfileSchema().bind(
687 username=c.user.username, user_emails=c.user.emails)
688 form = forms.RcForm(
689 schema, buttons=(forms.buttons.save, forms.buttons.reset))
690
691 controls = self.request.POST.items()
692 try:
693 valid_data = form.validate(controls)
694 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
695 'new_password', 'password_confirmation']
696 if c.extern_type != "rhodecode":
697 # forbid updating username for external accounts
698 skip_attrs.append('username')
699 old_email = c.user.email
700 UserModel().update_user(
701 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
702 **valid_data)
703 if old_email != valid_data['email']:
704 old = UserEmailMap.query() \
705 .filter(UserEmailMap.user == c.user)\
706 .filter(UserEmailMap.email == valid_data['email'])\
707 .first()
708 old.email = old_email
709 h.flash(_('Your account was updated successfully'), category='success')
710 Session().commit()
711 except forms.ValidationFailure as e:
712 c.form = e
713 return self._get_template_context(c)
714 except Exception:
715 log.exception("Exception updating user")
716 h.flash(_('Error occurred during update of user'),
717 category='error')
718 raise HTTPFound(h.route_path('my_account_profile'))
719
720 657 def _get_pull_requests_list(self, statuses):
721 658 draw, start, limit = self._extract_chunk(self.request)
722 659 search_q, order_by, order_dir = self._extract_ordering(self.request)
723 660
724 661 _render = self.request.get_partial_renderer(
725 662 'rhodecode:templates/data_table/_dt_elements.mako')
726 663
727 664 pull_requests = PullRequestModel().get_im_participating_in(
728 665 user_id=self._rhodecode_user.user_id,
729 666 statuses=statuses, query=search_q,
730 667 offset=start, length=limit, order_by=order_by,
731 668 order_dir=order_dir)
732 669
733 670 pull_requests_total_count = PullRequestModel().count_im_participating_in(
734 671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
735 672
736 673 data = []
737 674 comments_model = CommentsModel()
738 675 for pr in pull_requests:
739 676 repo_id = pr.target_repo_id
740 677 comments_count = comments_model.get_all_comments(
741 678 repo_id, pull_request=pr, include_drafts=False, count_only=True)
742 679 owned = pr.user_id == self._rhodecode_user.user_id
743 680
744 681 data.append({
745 682 'target_repo': _render('pullrequest_target_repo',
746 683 pr.target_repo.repo_name),
747 684 'name': _render('pullrequest_name',
748 685 pr.pull_request_id, pr.pull_request_state,
749 686 pr.work_in_progress, pr.target_repo.repo_name,
750 687 short=True),
751 688 'name_raw': pr.pull_request_id,
752 689 'status': _render('pullrequest_status',
753 690 pr.calculated_review_status()),
754 691 'title': _render('pullrequest_title', pr.title, pr.description),
755 692 'description': h.escape(pr.description),
756 693 'updated_on': _render('pullrequest_updated_on',
757 694 h.datetime_to_time(pr.updated_on),
758 695 pr.versions_count),
759 696 'updated_on_raw': h.datetime_to_time(pr.updated_on),
760 697 'created_on': _render('pullrequest_updated_on',
761 698 h.datetime_to_time(pr.created_on)),
762 699 'created_on_raw': h.datetime_to_time(pr.created_on),
763 700 'state': pr.pull_request_state,
764 701 'author': _render('pullrequest_author',
765 702 pr.author.full_contact, ),
766 703 'author_raw': pr.author.full_name,
767 704 'comments': _render('pullrequest_comments', comments_count),
768 705 'comments_raw': comments_count,
769 706 'closed': pr.is_closed(),
770 707 'owned': owned
771 708 })
772 709
773 710 # json used to render the grid
774 711 data = ({
775 712 'draw': draw,
776 713 'data': data,
777 714 'recordsTotal': pull_requests_total_count,
778 715 'recordsFiltered': pull_requests_total_count,
779 716 })
780 717 return data
781 718
782 719 @LoginRequired()
783 720 @NotAnonymous()
784 @view_config(
785 route_name='my_account_pullrequests',
786 request_method='GET',
787 renderer='rhodecode:templates/admin/my_account/my_account.mako')
788 721 def my_account_pullrequests(self):
789 722 c = self.load_default_context()
790 723 c.active = 'pullrequests'
791 724 req_get = self.request.GET
792 725
793 726 c.closed = str2bool(req_get.get('pr_show_closed'))
794 727
795 728 return self._get_template_context(c)
796 729
797 730 @LoginRequired()
798 731 @NotAnonymous()
799 @view_config(
800 route_name='my_account_pullrequests_data',
801 request_method='GET', renderer='json_ext')
802 732 def my_account_pullrequests_data(self):
803 733 self.load_default_context()
804 734 req_get = self.request.GET
805 735 closed = str2bool(req_get.get('closed'))
806 736
807 737 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
808 738 if closed:
809 739 statuses += [PullRequest.STATUS_CLOSED]
810 740
811 741 data = self._get_pull_requests_list(statuses=statuses)
812 742 return data
813 743
814 744 @LoginRequired()
815 745 @NotAnonymous()
816 @view_config(
817 route_name='my_account_user_group_membership',
818 request_method='GET',
819 renderer='rhodecode:templates/admin/my_account/my_account.mako')
820 746 def my_account_user_group_membership(self):
821 747 c = self.load_default_context()
822 748 c.active = 'user_group_membership'
823 749 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
824 750 for group in self._rhodecode_db_user.group_member]
825 751 c.user_groups = json.dumps(groups)
826 752 return self._get_template_context(c)
@@ -1,201 +1,185 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 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
23 23 from pyramid.httpexceptions import (
24 24 HTTPFound, HTTPNotFound, HTTPInternalServerError)
25 from pyramid.view import view_config
26 25
27 26 from rhodecode.apps._base import BaseAppView
28 27 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
29 28
30 29 from rhodecode.lib import helpers as h
31 30 from rhodecode.lib.helpers import SqlPage
32 31 from rhodecode.lib.utils2 import safe_int
33 32 from rhodecode.model.db import Notification
34 33 from rhodecode.model.notification import NotificationModel
35 34 from rhodecode.model.meta import Session
36 35
37 36
38 37 log = logging.getLogger(__name__)
39 38
40 39
41 40 class MyAccountNotificationsView(BaseAppView):
42 41
43 42 def load_default_context(self):
44 43 c = self._get_local_tmpl_context()
45 44 c.user = c.auth_user.get_instance()
46 45
47 46 return c
48 47
49 48 def _has_permissions(self, notification):
50 49 def is_owner():
51 50 user_id = self._rhodecode_db_user.user_id
52 51 for user_notification in notification.notifications_to_users:
53 52 if user_notification.user.user_id == user_id:
54 53 return True
55 54 return False
56 55 return h.HasPermissionAny('hg.admin')() or is_owner()
57 56
58 57 @LoginRequired()
59 58 @NotAnonymous()
60 @view_config(
61 route_name='notifications_show_all', request_method='GET',
62 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
63 59 def notifications_show_all(self):
64 60 c = self.load_default_context()
65 61
66 62 c.unread_count = NotificationModel().get_unread_cnt_for_user(
67 63 self._rhodecode_db_user.user_id)
68 64
69 65 _current_filter = self.request.GET.getall('type') or ['unread']
70 66
71 67 notifications = NotificationModel().get_for_user(
72 68 self._rhodecode_db_user.user_id,
73 69 filter_=_current_filter)
74 70
75 71 p = safe_int(self.request.GET.get('page', 1), 1)
76 72
77 73 def url_generator(page_num):
78 74 query_params = {
79 75 'page': page_num
80 76 }
81 77 _query = self.request.GET.mixed()
82 78 query_params.update(_query)
83 79 return self.request.current_route_path(_query=query_params)
84 80
85 81 c.notifications = SqlPage(notifications, page=p, items_per_page=10,
86 82 url_maker=url_generator)
87 83
88 84 c.unread_type = 'unread'
89 85 c.all_type = 'all'
90 86 c.pull_request_type = Notification.TYPE_PULL_REQUEST
91 87 c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
92 88 Notification.TYPE_PULL_REQUEST_COMMENT]
93 89
94 90 c.current_filter = 'unread' # default filter
95 91
96 92 if _current_filter == [c.pull_request_type]:
97 93 c.current_filter = 'pull_request'
98 94 elif _current_filter == c.comment_type:
99 95 c.current_filter = 'comment'
100 96 elif _current_filter == [c.unread_type]:
101 97 c.current_filter = 'unread'
102 98 elif _current_filter == [c.all_type]:
103 99 c.current_filter = 'all'
104 100 return self._get_template_context(c)
105 101
106 102 @LoginRequired()
107 103 @NotAnonymous()
108 @CSRFRequired()
109 @view_config(
110 route_name='notifications_mark_all_read', request_method='POST',
111 renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
112 def notifications_mark_all_read(self):
113 NotificationModel().mark_all_read_for_user(
114 self._rhodecode_db_user.user_id,
115 filter_=self.request.GET.getall('type'))
116 Session().commit()
117 raise HTTPFound(h.route_path('notifications_show_all'))
118
119 @LoginRequired()
120 @NotAnonymous()
121 @view_config(
122 route_name='notifications_show', request_method='GET',
123 renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
124 104 def notifications_show(self):
125 105 c = self.load_default_context()
126 106 notification_id = self.request.matchdict['notification_id']
127 107 notification = Notification.get_or_404(notification_id)
128 108
129 109 if not self._has_permissions(notification):
130 110 log.debug('User %s does not have permission to access notification',
131 111 self._rhodecode_user)
132 112 raise HTTPNotFound()
133 113
134 114 u_notification = NotificationModel().get_user_notification(
135 115 self._rhodecode_db_user.user_id, notification)
136 116 if not u_notification:
137 117 log.debug('User %s notification does not exist',
138 118 self._rhodecode_user)
139 119 raise HTTPNotFound()
140 120
141 121 # when opening this notification, mark it as read for this use
142 122 if not u_notification.read:
143 123 u_notification.mark_as_read()
144 124 Session().commit()
145 125
146 126 c.notification = notification
147 127
148 128 return self._get_template_context(c)
149 129
150 130 @LoginRequired()
151 131 @NotAnonymous()
152 132 @CSRFRequired()
153 @view_config(
154 route_name='notifications_update', request_method='POST',
155 renderer='json_ext')
133 def notifications_mark_all_read(self):
134 NotificationModel().mark_all_read_for_user(
135 self._rhodecode_db_user.user_id,
136 filter_=self.request.GET.getall('type'))
137 Session().commit()
138 raise HTTPFound(h.route_path('notifications_show_all'))
139
140 @LoginRequired()
141 @NotAnonymous()
142 @CSRFRequired()
156 143 def notification_update(self):
157 144 notification_id = self.request.matchdict['notification_id']
158 145 notification = Notification.get_or_404(notification_id)
159 146
160 147 if not self._has_permissions(notification):
161 148 log.debug('User %s does not have permission to access notification',
162 149 self._rhodecode_user)
163 150 raise HTTPNotFound()
164 151
165 152 try:
166 153 # updates notification read flag
167 154 NotificationModel().mark_read(
168 155 self._rhodecode_user.user_id, notification)
169 156 Session().commit()
170 157 return 'ok'
171 158 except Exception:
172 159 Session().rollback()
173 160 log.exception("Exception updating a notification item")
174 161
175 162 raise HTTPInternalServerError()
176 163
177 164 @LoginRequired()
178 165 @NotAnonymous()
179 166 @CSRFRequired()
180 @view_config(
181 route_name='notifications_delete', request_method='POST',
182 renderer='json_ext')
183 167 def notification_delete(self):
184 168 notification_id = self.request.matchdict['notification_id']
185 169 notification = Notification.get_or_404(notification_id)
186 170 if not self._has_permissions(notification):
187 171 log.debug('User %s does not have permission to access notification',
188 172 self._rhodecode_user)
189 173 raise HTTPNotFound()
190 174
191 175 try:
192 176 # deletes only notification2user
193 177 NotificationModel().delete(
194 178 self._rhodecode_user.user_id, notification)
195 179 Session().commit()
196 180 return 'ok'
197 181 except Exception:
198 182 Session().rollback()
199 183 log.exception("Exception deleting a notification item")
200 184
201 185 raise HTTPInternalServerError()
@@ -1,159 +1,146 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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
23 23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25 24
26 25 from rhodecode.apps._base import BaseAppView, DataGridAppView
27 26 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
28 27 from rhodecode.events import trigger
29 28 from rhodecode.lib import helpers as h
30 29 from rhodecode.lib import audit_logger
31 30 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
32 31 from rhodecode.model.db import IntegrityError, UserSshKeys
33 32 from rhodecode.model.meta import Session
34 33 from rhodecode.model.ssh_key import SshKeyModel
35 34
36 35 log = logging.getLogger(__name__)
37 36
38 37
39 38 class MyAccountSshKeysView(BaseAppView, DataGridAppView):
40 39
41 40 def load_default_context(self):
42 41 c = self._get_local_tmpl_context()
43 42 c.user = c.auth_user.get_instance()
44
45 43 c.ssh_enabled = self.request.registry.settings.get(
46 44 'ssh.generate_authorized_keyfile')
47
48 45 return c
49 46
50 47 @LoginRequired()
51 48 @NotAnonymous()
52 @view_config(
53 route_name='my_account_ssh_keys', request_method='GET',
54 renderer='rhodecode:templates/admin/my_account/my_account.mako')
55 49 def my_account_ssh_keys(self):
56 50 _ = self.request.translate
57 51
58 52 c = self.load_default_context()
59 53 c.active = 'ssh_keys'
60 54 c.default_key = self.request.GET.get('default_key')
61 55 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
62 56 return self._get_template_context(c)
63 57
64 58 @LoginRequired()
65 59 @NotAnonymous()
66 @view_config(
67 route_name='my_account_ssh_keys_generate', request_method='GET',
68 renderer='rhodecode:templates/admin/my_account/my_account.mako')
69 60 def ssh_keys_generate_keypair(self):
70 61 _ = self.request.translate
71 62 c = self.load_default_context()
72 63
73 64 c.active = 'ssh_keys_generate'
74 65 if c.ssh_key_generator_enabled:
75 66 private_format = self.request.GET.get('private_format') \
76 67 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
77 68 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
78 69 c.private, c.public = SshKeyModel().generate_keypair(
79 70 comment=comment, private_format=private_format)
80 71 c.target_form_url = h.route_path(
81 72 'my_account_ssh_keys', _query=dict(default_key=c.public))
82 73 return self._get_template_context(c)
83 74
84 75 @LoginRequired()
85 76 @NotAnonymous()
86 77 @CSRFRequired()
87 @view_config(
88 route_name='my_account_ssh_keys_add', request_method='POST',)
89 78 def my_account_ssh_keys_add(self):
90 79 _ = self.request.translate
91 80 c = self.load_default_context()
92 81
93 82 user_data = c.user.get_api_data()
94 83 key_data = self.request.POST.get('key_data')
95 84 description = self.request.POST.get('description')
96 85 fingerprint = 'unknown'
97 86 try:
98 87 if not key_data:
99 88 raise ValueError('Please add a valid public key')
100 89
101 90 key = SshKeyModel().parse_key(key_data.strip())
102 91 fingerprint = key.hash_md5()
103 92
104 93 ssh_key = SshKeyModel().create(
105 94 c.user.user_id, fingerprint, key.keydata, description)
106 95 ssh_key_data = ssh_key.get_api_data()
107 96
108 97 audit_logger.store_web(
109 98 'user.edit.ssh_key.add', action_data={
110 99 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
111 100 user=self._rhodecode_user, )
112 101 Session().commit()
113 102
114 103 # Trigger an event on change of keys.
115 104 trigger(SshKeyFileChangeEvent(), self.request.registry)
116 105
117 106 h.flash(_("Ssh Key successfully created"), category='success')
118 107
119 108 except IntegrityError:
120 109 log.exception("Exception during ssh key saving")
121 110 err = 'Such key with fingerprint `{}` already exists, ' \
122 111 'please use a different one'.format(fingerprint)
123 112 h.flash(_('An error occurred during ssh key saving: {}').format(err),
124 113 category='error')
125 114 except Exception as e:
126 115 log.exception("Exception during ssh key saving")
127 116 h.flash(_('An error occurred during ssh key saving: {}').format(e),
128 117 category='error')
129 118
130 119 return HTTPFound(h.route_path('my_account_ssh_keys'))
131 120
132 121 @LoginRequired()
133 122 @NotAnonymous()
134 123 @CSRFRequired()
135 @view_config(
136 route_name='my_account_ssh_keys_delete', request_method='POST')
137 124 def my_account_ssh_keys_delete(self):
138 125 _ = self.request.translate
139 126 c = self.load_default_context()
140 127
141 128 user_data = c.user.get_api_data()
142 129
143 130 del_ssh_key = self.request.POST.get('del_ssh_key')
144 131
145 132 if del_ssh_key:
146 133 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
147 134 ssh_key_data = ssh_key.get_api_data()
148 135
149 136 SshKeyModel().delete(del_ssh_key, c.user.user_id)
150 137 audit_logger.store_web(
151 138 'user.edit.ssh_key.delete', action_data={
152 139 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
153 140 user=self._rhodecode_user,)
154 141 Session().commit()
155 142 # Trigger an event on change of keys.
156 143 trigger(SshKeyFileChangeEvent(), self.request.registry)
157 144 h.flash(_("Ssh key successfully deleted"), category='success')
158 145
159 146 return HTTPFound(h.route_path('my_account_ssh_keys'))
@@ -1,46 +1,56 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import ADMIN_PREFIX
22 22
23 23
24 24 def admin_routes(config):
25 from rhodecode.apps.ops.views import OpsView
26
25 27 config.add_route(
26 28 name='ops_ping',
27 29 pattern='/ping')
30 config.add_view(
31 OpsView,
32 attr='ops_ping',
33 route_name='ops_ping', request_method='GET',
34 renderer='json_ext')
35
28 36 config.add_route(
29 37 name='ops_error_test',
30 38 pattern='/error')
39 config.add_view(
40 OpsView,
41 attr='ops_error_test',
42 route_name='ops_error_test', request_method='GET',
43 renderer='json_ext')
44
31 45 config.add_route(
32 46 name='ops_redirect_test',
33 47 pattern='/redirect')
48 config.add_view(
49 OpsView,
50 attr='ops_redirect_test',
51 route_name='ops_redirect_test', request_method='GET',
52 renderer='json_ext')
34 53
35 54
36 55 def includeme(config):
37
38 56 config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
39 # make OLD entries from <4.10.0 work
40 config.add_route(
41 name='ops_ping_legacy', pattern=ADMIN_PREFIX + '/ping')
42 config.add_route(
43 name='ops_error_test_legacy', pattern=ADMIN_PREFIX + '/error_test')
44
45 # Scan module for configuration decorators.
46 config.scan('.views', ignore='.tests')
@@ -1,89 +1,74 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 import logging
23 23
24 from pyramid.view import view_config
24
25 25 from pyramid.httpexceptions import HTTPFound
26 26
27 27 from rhodecode.apps._base import BaseAppView
28 28 from rhodecode.lib import helpers as h
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class OpsView(BaseAppView):
34 34
35 35 def load_default_context(self):
36 36 c = self._get_local_tmpl_context()
37 37 c.user = c.auth_user.get_instance()
38 38
39 39 return c
40 40
41 @view_config(
42 route_name='ops_ping', request_method='GET',
43 renderer='json_ext')
44 @view_config(
45 route_name='ops_ping_legacy', request_method='GET',
46 renderer='json_ext')
47 41 def ops_ping(self):
48 42 data = {
49 43 'instance': self.request.registry.settings.get('instance_id'),
50 44 }
51 45 if getattr(self.request, 'user'):
52 46 caller_name = 'anonymous'
53 47 if self.request.user.user_id:
54 48 caller_name = self.request.user.username
55 49
56 50 data.update({
57 51 'caller_ip': self.request.user.ip_addr,
58 52 'caller_name': caller_name,
59 53 })
60 54 return {'ok': data}
61 55
62 @view_config(
63 route_name='ops_error_test', request_method='GET',
64 renderer='json_ext')
65 @view_config(
66 route_name='ops_error_test_legacy', request_method='GET',
67 renderer='json_ext')
68 56 def ops_error_test(self):
69 57 """
70 58 Test exception handling and emails on errors
71 59 """
72 60
73 61 class TestException(Exception):
74 62 pass
75 63 # add timeout so we add some sort of rate limiter
76 64 time.sleep(2)
77 65 msg = ('RhodeCode Enterprise test exception. '
78 66 'Client:{}. Generation time: {}.'.format(self.request.user, time.time()))
79 67 raise TestException(msg)
80 68
81 @view_config(
82 route_name='ops_redirect_test', request_method='GET',
83 renderer='json_ext')
84 69 def ops_redirect_test(self):
85 70 """
86 71 Test redirect handling
87 72 """
88 73 redirect_to = self.request.GET.get('to') or h.route_path('home')
89 74 raise HTTPFound(redirect_to)
@@ -1,61 +1,102 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import add_route_with_slash
21 from rhodecode.apps.repo_group.views.repo_group_settings import RepoGroupSettingsView
22 from rhodecode.apps.repo_group.views.repo_group_advanced import RepoGroupAdvancedSettingsView
23 from rhodecode.apps.repo_group.views.repo_group_permissions import RepoGroupPermissionsView
24 from rhodecode.apps.home.views import HomeView
21 25
22 26
23 27 def includeme(config):
24 28
25 29 # Settings
26 30 config.add_route(
27 31 name='edit_repo_group',
28 32 pattern='/{repo_group_name:.*?[^/]}/_edit',
29 33 repo_group_route=True)
30 # update is POST on edit_repo_group
34 config.add_view(
35 RepoGroupSettingsView,
36 attr='edit_settings',
37 route_name='edit_repo_group', request_method='GET',
38 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
39 config.add_view(
40 RepoGroupSettingsView,
41 attr='edit_settings_update',
42 route_name='edit_repo_group', request_method='POST',
43 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
31 44
32 45 # Settings advanced
33 46 config.add_route(
34 47 name='edit_repo_group_advanced',
35 48 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced',
36 49 repo_group_route=True)
50 config.add_view(
51 RepoGroupAdvancedSettingsView,
52 attr='edit_repo_group_advanced',
53 route_name='edit_repo_group_advanced', request_method='GET',
54 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
37 55
38 56 config.add_route(
39 57 name='edit_repo_group_advanced_delete',
40 58 pattern='/{repo_group_name:.*?[^/]}/_settings/advanced/delete',
41 59 repo_group_route=True)
60 config.add_view(
61 RepoGroupAdvancedSettingsView,
62 attr='edit_repo_group_delete',
63 route_name='edit_repo_group_advanced_delete', request_method='POST',
64 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
42 65
43 66 # settings permissions
44 67 config.add_route(
45 68 name='edit_repo_group_perms',
46 69 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions',
47 70 repo_group_route=True)
71 config.add_view(
72 RepoGroupPermissionsView,
73 attr='edit_repo_group_permissions',
74 route_name='edit_repo_group_perms', request_method='GET',
75 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
48 76
49 77 config.add_route(
50 78 name='edit_repo_group_perms_update',
51 79 pattern='/{repo_group_name:.*?[^/]}/_settings/permissions/update',
52 80 repo_group_route=True)
81 config.add_view(
82 RepoGroupPermissionsView,
83 attr='edit_repo_groups_permissions_update',
84 route_name='edit_repo_group_perms_update', request_method='POST',
85 renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
53 86
54 87 # Summary, NOTE(marcink): needs to be at the end for catch-all
55 88 add_route_with_slash(
56 89 config,
57 90 name='repo_group_home',
58 91 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
92 config.add_view(
93 HomeView,
94 attr='repo_group_main_page',
95 route_name='repo_group_home', request_method='GET',
96 renderer='rhodecode:templates/index_repo_group.mako')
97 config.add_view(
98 HomeView,
99 attr='repo_group_main_page',
100 route_name='repo_group_home_slash', request_method='GET',
101 renderer='rhodecode:templates/index_repo_group.mako')
59 102
60 # Scan module for configuration decorators.
61 config.scan('.views', ignore='.tests')
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now