##// END OF EJS Templates
security: update lastactivity when on audit logs....
marcink -
r2930:a5198975 default
parent child Browse files
Show More
@@ -1,542 +1,543 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import inspect
22 22 import itertools
23 23 import logging
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.ext_json import json
42 42 from rhodecode.lib.utils2 import safe_str
43 43 from rhodecode.lib.plugins.utils import get_plugin_settings
44 44 from rhodecode.model.db import User, UserApiKeys
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48 DEFAULT_RENDERER = 'jsonrpc_renderer'
49 49 DEFAULT_URL = '/_admin/apiv2'
50 50
51 51
52 52 def find_methods(jsonrpc_methods, pattern):
53 53 matches = OrderedDict()
54 54 if not isinstance(pattern, (list, tuple)):
55 55 pattern = [pattern]
56 56
57 57 for single_pattern in pattern:
58 58 for method_name, method in jsonrpc_methods.items():
59 59 if fnmatch.fnmatch(method_name, single_pattern):
60 60 matches[method_name] = method
61 61 return matches
62 62
63 63
64 64 class ExtJsonRenderer(object):
65 65 """
66 66 Custom renderer that mkaes use of our ext_json lib
67 67
68 68 """
69 69
70 70 def __init__(self, serializer=json.dumps, **kw):
71 71 """ Any keyword arguments will be passed to the ``serializer``
72 72 function."""
73 73 self.serializer = serializer
74 74 self.kw = kw
75 75
76 76 def __call__(self, info):
77 77 """ Returns a plain JSON-encoded string with content-type
78 78 ``application/json``. The content-type may be overridden by
79 79 setting ``request.response.content_type``."""
80 80
81 81 def _render(value, system):
82 82 request = system.get('request')
83 83 if request is not None:
84 84 response = request.response
85 85 ct = response.content_type
86 86 if ct == response.default_content_type:
87 87 response.content_type = 'application/json'
88 88
89 89 return self.serializer(value, **self.kw)
90 90
91 91 return _render
92 92
93 93
94 94 def jsonrpc_response(request, result):
95 95 rpc_id = getattr(request, 'rpc_id', None)
96 96 response = request.response
97 97
98 98 # store content_type before render is called
99 99 ct = response.content_type
100 100
101 101 ret_value = ''
102 102 if rpc_id:
103 103 ret_value = {
104 104 'id': rpc_id,
105 105 'result': result,
106 106 'error': None,
107 107 }
108 108
109 109 # fetch deprecation warnings, and store it inside results
110 110 deprecation = getattr(request, 'rpc_deprecation', None)
111 111 if deprecation:
112 112 ret_value['DEPRECATION_WARNING'] = deprecation
113 113
114 114 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
115 115 response.body = safe_str(raw_body, response.charset)
116 116
117 117 if ct == response.default_content_type:
118 118 response.content_type = 'application/json'
119 119
120 120 return response
121 121
122 122
123 123 def jsonrpc_error(request, message, retid=None, code=None):
124 124 """
125 125 Generate a Response object with a JSON-RPC error body
126 126
127 127 :param code:
128 128 :param retid:
129 129 :param message:
130 130 """
131 131 err_dict = {'id': retid, 'result': None, 'error': message}
132 132 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
133 133 return Response(
134 134 body=body,
135 135 status=code,
136 136 content_type='application/json'
137 137 )
138 138
139 139
140 140 def exception_view(exc, request):
141 141 rpc_id = getattr(request, 'rpc_id', None)
142 142
143 143 fault_message = 'undefined error'
144 144 if isinstance(exc, JSONRPCError):
145 145 fault_message = exc.message
146 146 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
147 147 elif isinstance(exc, JSONRPCValidationError):
148 148 colander_exc = exc.colander_exception
149 149 # TODO(marcink): think maybe of nicer way to serialize errors ?
150 150 fault_message = colander_exc.asdict()
151 151 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
152 152 elif isinstance(exc, JSONRPCForbidden):
153 153 fault_message = 'Access was denied to this resource.'
154 154 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
155 155 elif isinstance(exc, HTTPNotFound):
156 156 method = request.rpc_method
157 157 log.debug('json-rpc method `%s` not found in list of '
158 158 'api calls: %s, rpc_id:%s',
159 159 method, request.registry.jsonrpc_methods.keys(), rpc_id)
160 160
161 161 similar = 'none'
162 162 try:
163 163 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
164 164 similar_found = find_methods(
165 165 request.registry.jsonrpc_methods, similar_paterns)
166 166 similar = ', '.join(similar_found.keys()) or similar
167 167 except Exception:
168 168 # make the whole above block safe
169 169 pass
170 170
171 171 fault_message = "No such method: {}. Similar methods: {}".format(
172 172 method, similar)
173 173
174 174 return jsonrpc_error(request, fault_message, rpc_id)
175 175
176 176
177 177 def request_view(request):
178 178 """
179 179 Main request handling method. It handles all logic to call a specific
180 180 exposed method
181 181 """
182 182
183 183 # check if we can find this session using api_key, get_by_auth_token
184 184 # search not expired tokens only
185 185
186 186 try:
187 187 api_user = User.get_by_auth_token(request.rpc_api_key)
188 188
189 189 if api_user is None:
190 190 return jsonrpc_error(
191 191 request, retid=request.rpc_id, message='Invalid API KEY')
192 192
193 193 if not api_user.active:
194 194 return jsonrpc_error(
195 195 request, retid=request.rpc_id,
196 196 message='Request from this user not allowed')
197 197
198 198 # check if we are allowed to use this IP
199 199 auth_u = AuthUser(
200 200 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
201 201 if not auth_u.ip_allowed:
202 202 return jsonrpc_error(
203 203 request, retid=request.rpc_id,
204 204 message='Request from IP:%s not allowed' % (
205 205 request.rpc_ip_addr,))
206 206 else:
207 207 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
208 208
209 209 # register our auth-user
210 210 request.rpc_user = auth_u
211 request.environ['rc_auth_user_id'] = auth_u.user_id
211 212
212 213 # now check if token is valid for API
213 214 auth_token = request.rpc_api_key
214 215 token_match = api_user.authenticate_by_token(
215 216 auth_token, roles=[UserApiKeys.ROLE_API])
216 217 invalid_token = not token_match
217 218
218 219 log.debug('Checking if API KEY is valid with proper role')
219 220 if invalid_token:
220 221 return jsonrpc_error(
221 222 request, retid=request.rpc_id,
222 223 message='API KEY invalid or, has bad role for an API call')
223 224
224 225 except Exception:
225 226 log.exception('Error on API AUTH')
226 227 return jsonrpc_error(
227 228 request, retid=request.rpc_id, message='Invalid API KEY')
228 229
229 230 method = request.rpc_method
230 231 func = request.registry.jsonrpc_methods[method]
231 232
232 233 # now that we have a method, add request._req_params to
233 234 # self.kargs and dispatch control to WGIController
234 235 argspec = inspect.getargspec(func)
235 236 arglist = argspec[0]
236 237 defaults = map(type, argspec[3] or [])
237 238 default_empty = types.NotImplementedType
238 239
239 240 # kw arguments required by this method
240 241 func_kwargs = dict(itertools.izip_longest(
241 242 reversed(arglist), reversed(defaults), fillvalue=default_empty))
242 243
243 244 # This attribute will need to be first param of a method that uses
244 245 # api_key, which is translated to instance of user at that name
245 246 user_var = 'apiuser'
246 247 request_var = 'request'
247 248
248 249 for arg in [user_var, request_var]:
249 250 if arg not in arglist:
250 251 return jsonrpc_error(
251 252 request,
252 253 retid=request.rpc_id,
253 254 message='This method [%s] does not support '
254 255 'required parameter `%s`' % (func.__name__, arg))
255 256
256 257 # get our arglist and check if we provided them as args
257 258 for arg, default in func_kwargs.items():
258 259 if arg in [user_var, request_var]:
259 260 # user_var and request_var are pre-hardcoded parameters and we
260 261 # don't need to do any translation
261 262 continue
262 263
263 264 # skip the required param check if it's default value is
264 265 # NotImplementedType (default_empty)
265 266 if default == default_empty and arg not in request.rpc_params:
266 267 return jsonrpc_error(
267 268 request,
268 269 retid=request.rpc_id,
269 270 message=('Missing non optional `%s` arg in JSON DATA' % arg)
270 271 )
271 272
272 273 # sanitize extra passed arguments
273 274 for k in request.rpc_params.keys()[:]:
274 275 if k not in func_kwargs:
275 276 del request.rpc_params[k]
276 277
277 278 call_params = request.rpc_params
278 279 call_params.update({
279 280 'request': request,
280 281 'apiuser': auth_u
281 282 })
282 283
283 284 # register some common functions for usage
284 285 attach_context_attributes(
285 286 TemplateArgs(), request, request.rpc_user.user_id)
286 287
287 288 try:
288 289 ret_value = func(**call_params)
289 290 return jsonrpc_response(request, ret_value)
290 291 except JSONRPCBaseError:
291 292 raise
292 293 except Exception:
293 294 log.exception('Unhandled exception occurred on api call: %s', func)
294 295 return jsonrpc_error(request, retid=request.rpc_id,
295 296 message='Internal server error')
296 297
297 298
298 299 def setup_request(request):
299 300 """
300 301 Parse a JSON-RPC request body. It's used inside the predicates method
301 302 to validate and bootstrap requests for usage in rpc calls.
302 303
303 304 We need to raise JSONRPCError here if we want to return some errors back to
304 305 user.
305 306 """
306 307
307 308 log.debug('Executing setup request: %r', request)
308 309 request.rpc_ip_addr = get_ip_addr(request.environ)
309 310 # TODO(marcink): deprecate GET at some point
310 311 if request.method not in ['POST', 'GET']:
311 312 log.debug('unsupported request method "%s"', request.method)
312 313 raise JSONRPCError(
313 314 'unsupported request method "%s". Please use POST' % request.method)
314 315
315 316 if 'CONTENT_LENGTH' not in request.environ:
316 317 log.debug("No Content-Length")
317 318 raise JSONRPCError("Empty body, No Content-Length in request")
318 319
319 320 else:
320 321 length = request.environ['CONTENT_LENGTH']
321 322 log.debug('Content-Length: %s', length)
322 323
323 324 if length == 0:
324 325 log.debug("Content-Length is 0")
325 326 raise JSONRPCError("Content-Length is 0")
326 327
327 328 raw_body = request.body
328 329 try:
329 330 json_body = json.loads(raw_body)
330 331 except ValueError as e:
331 332 # catch JSON errors Here
332 333 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
333 334
334 335 request.rpc_id = json_body.get('id')
335 336 request.rpc_method = json_body.get('method')
336 337
337 338 # check required base parameters
338 339 try:
339 340 api_key = json_body.get('api_key')
340 341 if not api_key:
341 342 api_key = json_body.get('auth_token')
342 343
343 344 if not api_key:
344 345 raise KeyError('api_key or auth_token')
345 346
346 347 # TODO(marcink): support passing in token in request header
347 348
348 349 request.rpc_api_key = api_key
349 350 request.rpc_id = json_body['id']
350 351 request.rpc_method = json_body['method']
351 352 request.rpc_params = json_body['args'] \
352 353 if isinstance(json_body['args'], dict) else {}
353 354
354 355 log.debug(
355 356 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
356 357 except KeyError as e:
357 358 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
358 359
359 360 log.debug('setup complete, now handling method:%s rpcid:%s',
360 361 request.rpc_method, request.rpc_id, )
361 362
362 363
363 364 class RoutePredicate(object):
364 365 def __init__(self, val, config):
365 366 self.val = val
366 367
367 368 def text(self):
368 369 return 'jsonrpc route = %s' % self.val
369 370
370 371 phash = text
371 372
372 373 def __call__(self, info, request):
373 374 if self.val:
374 375 # potentially setup and bootstrap our call
375 376 setup_request(request)
376 377
377 378 # Always return True so that even if it isn't a valid RPC it
378 379 # will fall through to the underlaying handlers like notfound_view
379 380 return True
380 381
381 382
382 383 class NotFoundPredicate(object):
383 384 def __init__(self, val, config):
384 385 self.val = val
385 386 self.methods = config.registry.jsonrpc_methods
386 387
387 388 def text(self):
388 389 return 'jsonrpc method not found = {}.'.format(self.val)
389 390
390 391 phash = text
391 392
392 393 def __call__(self, info, request):
393 394 return hasattr(request, 'rpc_method')
394 395
395 396
396 397 class MethodPredicate(object):
397 398 def __init__(self, val, config):
398 399 self.method = val
399 400
400 401 def text(self):
401 402 return 'jsonrpc method = %s' % self.method
402 403
403 404 phash = text
404 405
405 406 def __call__(self, context, request):
406 407 # we need to explicitly return False here, so pyramid doesn't try to
407 408 # execute our view directly. We need our main handler to execute things
408 409 return getattr(request, 'rpc_method') == self.method
409 410
410 411
411 412 def add_jsonrpc_method(config, view, **kwargs):
412 413 # pop the method name
413 414 method = kwargs.pop('method', None)
414 415
415 416 if method is None:
416 417 raise ConfigurationError(
417 418 'Cannot register a JSON-RPC method without specifying the '
418 419 '"method"')
419 420
420 421 # we define custom predicate, to enable to detect conflicting methods,
421 422 # those predicates are kind of "translation" from the decorator variables
422 423 # to internal predicates names
423 424
424 425 kwargs['jsonrpc_method'] = method
425 426
426 427 # register our view into global view store for validation
427 428 config.registry.jsonrpc_methods[method] = view
428 429
429 430 # we're using our main request_view handler, here, so each method
430 431 # has a unified handler for itself
431 432 config.add_view(request_view, route_name='apiv2', **kwargs)
432 433
433 434
434 435 class jsonrpc_method(object):
435 436 """
436 437 decorator that works similar to @add_view_config decorator,
437 438 but tailored for our JSON RPC
438 439 """
439 440
440 441 venusian = venusian # for testing injection
441 442
442 443 def __init__(self, method=None, **kwargs):
443 444 self.method = method
444 445 self.kwargs = kwargs
445 446
446 447 def __call__(self, wrapped):
447 448 kwargs = self.kwargs.copy()
448 449 kwargs['method'] = self.method or wrapped.__name__
449 450 depth = kwargs.pop('_depth', 0)
450 451
451 452 def callback(context, name, ob):
452 453 config = context.config.with_package(info.module)
453 454 config.add_jsonrpc_method(view=ob, **kwargs)
454 455
455 456 info = venusian.attach(wrapped, callback, category='pyramid',
456 457 depth=depth + 1)
457 458 if info.scope == 'class':
458 459 # ensure that attr is set if decorating a class method
459 460 kwargs.setdefault('attr', wrapped.__name__)
460 461
461 462 kwargs['_info'] = info.codeinfo # fbo action_method
462 463 return wrapped
463 464
464 465
465 466 class jsonrpc_deprecated_method(object):
466 467 """
467 468 Marks method as deprecated, adds log.warning, and inject special key to
468 469 the request variable to mark method as deprecated.
469 470 Also injects special docstring that extract_docs will catch to mark
470 471 method as deprecated.
471 472
472 473 :param use_method: specify which method should be used instead of
473 474 the decorated one
474 475
475 476 Use like::
476 477
477 478 @jsonrpc_method()
478 479 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
479 480 def old_func(request, apiuser, arg1, arg2):
480 481 ...
481 482 """
482 483
483 484 def __init__(self, use_method, deprecated_at_version):
484 485 self.use_method = use_method
485 486 self.deprecated_at_version = deprecated_at_version
486 487 self.deprecated_msg = ''
487 488
488 489 def __call__(self, func):
489 490 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
490 491 method=self.use_method)
491 492
492 493 docstring = """\n
493 494 .. deprecated:: {version}
494 495
495 496 {deprecation_message}
496 497
497 498 {original_docstring}
498 499 """
499 500 func.__doc__ = docstring.format(
500 501 version=self.deprecated_at_version,
501 502 deprecation_message=self.deprecated_msg,
502 503 original_docstring=func.__doc__)
503 504 return decorator.decorator(self.__wrapper, func)
504 505
505 506 def __wrapper(self, func, *fargs, **fkwargs):
506 507 log.warning('DEPRECATED API CALL on function %s, please '
507 508 'use `%s` instead', func, self.use_method)
508 509 # alter function docstring to mark as deprecated, this is picked up
509 510 # via fabric file that generates API DOC.
510 511 result = func(*fargs, **fkwargs)
511 512
512 513 request = fargs[0]
513 514 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
514 515 return result
515 516
516 517
517 518 def includeme(config):
518 519 plugin_module = 'rhodecode.api'
519 520 plugin_settings = get_plugin_settings(
520 521 plugin_module, config.registry.settings)
521 522
522 523 if not hasattr(config.registry, 'jsonrpc_methods'):
523 524 config.registry.jsonrpc_methods = OrderedDict()
524 525
525 526 # match filter by given method only
526 527 config.add_view_predicate('jsonrpc_method', MethodPredicate)
527 528
528 529 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
529 530 serializer=json.dumps, indent=4))
530 531 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
531 532
532 533 config.add_route_predicate(
533 534 'jsonrpc_call', RoutePredicate)
534 535
535 536 config.add_route(
536 537 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
537 538
538 539 config.scan(plugin_module, ignore='rhodecode.api.tests')
539 540 # register some exception handling view
540 541 config.add_view(exception_view, context=JSONRPCBaseError)
541 542 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
542 543 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,116 +1,120 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23
24 24 from rhodecode.model.db import User
25 25 from rhodecode.model.user import UserModel
26 26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_ok, assert_error, crash, jsonify)
29 29
30 30
31 31 @pytest.mark.usefixtures("testuser_api", "app")
32 32 class TestUpdateUser(object):
33 33 @pytest.mark.parametrize("name, expected", [
34 34 ('firstname', 'new_username'),
35 35 ('lastname', 'new_username'),
36 36 ('email', 'new_username'),
37 37 ('admin', True),
38 38 ('admin', False),
39 39 ('extern_type', 'ldap'),
40 40 ('extern_type', None),
41 41 ('extern_name', 'test'),
42 42 ('extern_name', None),
43 43 ('active', False),
44 44 ('active', True),
45 45 ('password', 'newpass')
46 46 ])
47 47 def test_api_update_user(self, name, expected, user_util):
48 48 usr = user_util.create_user()
49 49
50 50 kw = {name: expected, 'userid': usr.user_id}
51 51 id_, params = build_data(self.apikey, 'update_user', **kw)
52 52 response = api_call(self.app, params)
53 53
54 54 ret = {
55 55 'msg': 'updated user ID:%s %s' % (usr.user_id, usr.username),
56 56 'user': jsonify(
57 57 UserModel()
58 58 .get_by_username(usr.username)
59 59 .get_api_data(include_secrets=True)
60 60 )
61 61 }
62 62
63 63 expected = ret
64 64 assert_ok(id_, expected, given=response.body)
65 65
66 66 def test_api_update_user_no_changed_params(self):
67 67 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
68 68 ret = jsonify(usr.get_api_data(include_secrets=True))
69 69 id_, params = build_data(
70 70 self.apikey, 'update_user', userid=TEST_USER_ADMIN_LOGIN)
71 71
72 72 response = api_call(self.app, params)
73 73 ret = {
74 74 'msg': 'updated user ID:%s %s' % (
75 75 usr.user_id, TEST_USER_ADMIN_LOGIN),
76 76 'user': ret
77 77 }
78 78 expected = ret
79 expected['user']['last_activity'] = response.json['result']['user'][
80 'last_activity']
79 81 assert_ok(id_, expected, given=response.body)
80 82
81 83 def test_api_update_user_by_user_id(self):
82 84 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
83 85 ret = jsonify(usr.get_api_data(include_secrets=True))
84 86 id_, params = build_data(
85 87 self.apikey, 'update_user', userid=usr.user_id)
86 88
87 89 response = api_call(self.app, params)
88 90 ret = {
89 91 'msg': 'updated user ID:%s %s' % (
90 92 usr.user_id, TEST_USER_ADMIN_LOGIN),
91 93 'user': ret
92 94 }
93 95 expected = ret
96 expected['user']['last_activity'] = response.json['result']['user'][
97 'last_activity']
94 98 assert_ok(id_, expected, given=response.body)
95 99
96 100 def test_api_update_user_default_user(self):
97 101 usr = User.get_default_user()
98 102 id_, params = build_data(
99 103 self.apikey, 'update_user', userid=usr.user_id)
100 104
101 105 response = api_call(self.app, params)
102 106 expected = 'editing default user is forbidden'
103 107 assert_error(id_, expected, given=response.body)
104 108
105 109 @mock.patch.object(UserModel, 'update_user', crash)
106 110 def test_api_update_user_when_exception_happens(self):
107 111 usr = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
108 112 ret = jsonify(usr.get_api_data(include_secrets=True))
109 113 id_, params = build_data(
110 114 self.apikey, 'update_user', userid=usr.user_id)
111 115
112 116 response = api_call(self.app, params)
113 117 ret = 'failed to update user `%s`' % (usr.user_id,)
114 118
115 119 expected = ret
116 120 assert_error(id_, expected, given=response.body)
@@ -1,518 +1,522 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import logging
24 24 import collections
25 25 import tempfile
26 26
27 27 from paste.gzipper import make_gzip_middleware
28 import pyramid.events
28 29 from pyramid.wsgi import wsgiapp
29 30 from pyramid.authorization import ACLAuthorizationPolicy
30 31 from pyramid.config import Configurator
31 32 from pyramid.settings import asbool, aslist
32 33 from pyramid.httpexceptions import (
33 34 HTTPException, HTTPError, HTTPInternalServerError, HTTPFound, HTTPNotFound)
34 from pyramid.events import ApplicationCreated
35 35 from pyramid.renderers import render_to_response
36 36
37 37 from rhodecode.model import meta
38 38 from rhodecode.config import patches
39 39 from rhodecode.config import utils as config_utils
40 40 from rhodecode.config.environment import load_pyramid_environment
41 41
42 import rhodecode.events
42 43 from rhodecode.lib.middleware.vcs import VCSMiddleware
43 44 from rhodecode.lib.request import Request
44 45 from rhodecode.lib.vcs import VCSCommunicationError
45 46 from rhodecode.lib.exceptions import VCSServerUnavailable
46 47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
47 48 from rhodecode.lib.middleware.https_fixup import HttpsFixup
48 49 from rhodecode.lib.celerylib.loader import configure_celery
49 50 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
50 51 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
51 52 from rhodecode.lib.exc_tracking import store_exception
52 53 from rhodecode.subscribers import (
53 54 scan_repositories_if_enabled, write_js_routes_if_enabled,
54 55 write_metadata_if_needed, inject_app_settings)
55 56
56 57
57 58 log = logging.getLogger(__name__)
58 59
59 60
60 61 def is_http_error(response):
61 62 # error which should have traceback
62 63 return response.status_code > 499
63 64
64 65
65 66 def make_pyramid_app(global_config, **settings):
66 67 """
67 68 Constructs the WSGI application based on Pyramid.
68 69
69 70 Specials:
70 71
71 72 * The application can also be integrated like a plugin via the call to
72 73 `includeme`. This is accompanied with the other utility functions which
73 74 are called. Changing this should be done with great care to not break
74 75 cases when these fragments are assembled from another place.
75 76
76 77 """
77 78
78 79 # Allows to use format style "{ENV_NAME}" placeholders in the configuration. It
79 80 # will be replaced by the value of the environment variable "NAME" in this case.
80 81 environ = {
81 82 'ENV_{}'.format(key): value for key, value in os.environ.items()}
82 83
83 84 global_config = _substitute_values(global_config, environ)
84 85 settings = _substitute_values(settings, environ)
85 86
86 87 sanitize_settings_and_apply_defaults(settings)
87 88
88 89 config = Configurator(settings=settings)
89 90
90 91 # Apply compatibility patches
91 92 patches.inspect_getargspec()
92 93
93 94 load_pyramid_environment(global_config, settings)
94 95
95 96 # Static file view comes first
96 97 includeme_first(config)
97 98
98 99 includeme(config)
99 100
100 101 pyramid_app = config.make_wsgi_app()
101 102 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
102 103 pyramid_app.config = config
103 104
104 105 config.configure_celery(global_config['__file__'])
105 106 # creating the app uses a connection - return it after we are done
106 107 meta.Session.remove()
107 108
108 109 log.info('Pyramid app %s created and configured.', pyramid_app)
109 110 return pyramid_app
110 111
111 112
112 113 def not_found_view(request):
113 114 """
114 115 This creates the view which should be registered as not-found-view to
115 116 pyramid.
116 117 """
117 118
118 119 if not getattr(request, 'vcs_call', None):
119 120 # handle like regular case with our error_handler
120 121 return error_handler(HTTPNotFound(), request)
121 122
122 123 # handle not found view as a vcs call
123 124 settings = request.registry.settings
124 125 ae_client = getattr(request, 'ae_client', None)
125 126 vcs_app = VCSMiddleware(
126 127 HTTPNotFound(), request.registry, settings,
127 128 appenlight_client=ae_client)
128 129
129 130 return wsgiapp(vcs_app)(None, request)
130 131
131 132
132 133 def error_handler(exception, request):
133 134 import rhodecode
134 135 from rhodecode.lib import helpers
135 136
136 137 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
137 138
138 139 base_response = HTTPInternalServerError()
139 140 # prefer original exception for the response since it may have headers set
140 141 if isinstance(exception, HTTPException):
141 142 base_response = exception
142 143 elif isinstance(exception, VCSCommunicationError):
143 144 base_response = VCSServerUnavailable()
144 145
145 146 if is_http_error(base_response):
146 147 log.exception(
147 148 'error occurred handling this request for path: %s', request.path)
148 149
149 150 error_explanation = base_response.explanation or str(base_response)
150 151 if base_response.status_code == 404:
151 152 error_explanation += " Or you don't have permission to access it."
152 153 c = AttributeDict()
153 154 c.error_message = base_response.status
154 155 c.error_explanation = error_explanation
155 156 c.visual = AttributeDict()
156 157
157 158 c.visual.rhodecode_support_url = (
158 159 request.registry.settings.get('rhodecode_support_url') or
159 160 request.route_url('rhodecode_support')
160 161 )
161 162 c.redirect_time = 0
162 163 c.rhodecode_name = rhodecode_title
163 164 if not c.rhodecode_name:
164 165 c.rhodecode_name = 'Rhodecode'
165 166
166 167 c.causes = []
167 168 if is_http_error(base_response):
168 169 c.causes.append('Server is overloaded.')
169 170 c.causes.append('Server database connection is lost.')
170 171 c.causes.append('Server expected unhandled error.')
171 172
172 173 if hasattr(base_response, 'causes'):
173 174 c.causes = base_response.causes
174 175
175 176 c.messages = helpers.flash.pop_messages(request=request)
176 177
177 178 exc_info = sys.exc_info()
178 179 c.exception_id = id(exc_info)
179 180 c.show_exception_id = isinstance(base_response, VCSServerUnavailable) \
180 181 or base_response.status_code > 499
181 182 c.exception_id_url = request.route_url(
182 183 'admin_settings_exception_tracker_show', exception_id=c.exception_id)
183 184
184 185 if c.show_exception_id:
185 186 store_exception(c.exception_id, exc_info)
186 187
187 188 response = render_to_response(
188 189 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
189 190 response=base_response)
190 191
191 192 return response
192 193
193 194
194 195 def includeme_first(config):
195 196 # redirect automatic browser favicon.ico requests to correct place
196 197 def favicon_redirect(context, request):
197 198 return HTTPFound(
198 199 request.static_path('rhodecode:public/images/favicon.ico'))
199 200
200 201 config.add_view(favicon_redirect, route_name='favicon')
201 202 config.add_route('favicon', '/favicon.ico')
202 203
203 204 def robots_redirect(context, request):
204 205 return HTTPFound(
205 206 request.static_path('rhodecode:public/robots.txt'))
206 207
207 208 config.add_view(robots_redirect, route_name='robots')
208 209 config.add_route('robots', '/robots.txt')
209 210
210 211 config.add_static_view(
211 212 '_static/deform', 'deform:static')
212 213 config.add_static_view(
213 214 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
214 215
215 216
216 217 def includeme(config):
217 218 settings = config.registry.settings
218 219 config.set_request_factory(Request)
219 220
220 221 # plugin information
221 222 config.registry.rhodecode_plugins = collections.OrderedDict()
222 223
223 224 config.add_directive(
224 225 'register_rhodecode_plugin', register_rhodecode_plugin)
225 226
226 227 config.add_directive('configure_celery', configure_celery)
227 228
228 229 if asbool(settings.get('appenlight', 'false')):
229 230 config.include('appenlight_client.ext.pyramid_tween')
230 231
231 232 # Includes which are required. The application would fail without them.
232 233 config.include('pyramid_mako')
233 234 config.include('pyramid_beaker')
234 235 config.include('rhodecode.lib.caches')
235 236 config.include('rhodecode.lib.rc_cache')
236 237
237 238 config.include('rhodecode.authentication')
238 239 config.include('rhodecode.integrations')
239 240
240 241 # apps
241 242 config.include('rhodecode.apps._base')
242 243 config.include('rhodecode.apps.ops')
243 244
244 245 config.include('rhodecode.apps.admin')
245 246 config.include('rhodecode.apps.channelstream')
246 247 config.include('rhodecode.apps.login')
247 248 config.include('rhodecode.apps.home')
248 249 config.include('rhodecode.apps.journal')
249 250 config.include('rhodecode.apps.repository')
250 251 config.include('rhodecode.apps.repo_group')
251 252 config.include('rhodecode.apps.user_group')
252 253 config.include('rhodecode.apps.search')
253 254 config.include('rhodecode.apps.user_profile')
254 255 config.include('rhodecode.apps.user_group_profile')
255 256 config.include('rhodecode.apps.my_account')
256 257 config.include('rhodecode.apps.svn_support')
257 258 config.include('rhodecode.apps.ssh_support')
258 259 config.include('rhodecode.apps.gist')
259 260
260 261 config.include('rhodecode.apps.debug_style')
261 262 config.include('rhodecode.tweens')
262 263 config.include('rhodecode.api')
263 264
264 265 config.add_route(
265 266 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
266 267
267 268 config.add_translation_dirs('rhodecode:i18n/')
268 269 settings['default_locale_name'] = settings.get('lang', 'en')
269 270
270 271 # Add subscribers.
271 config.add_subscriber(inject_app_settings, ApplicationCreated)
272 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
273 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
274 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
275
272 config.add_subscriber(inject_app_settings,
273 pyramid.events.ApplicationCreated)
274 config.add_subscriber(scan_repositories_if_enabled,
275 pyramid.events.ApplicationCreated)
276 config.add_subscriber(write_metadata_if_needed,
277 pyramid.events.ApplicationCreated)
278 config.add_subscriber(write_js_routes_if_enabled,
279 pyramid.events.ApplicationCreated)
276 280
277 281 # request custom methods
278 282 config.add_request_method(
279 283 'rhodecode.lib.partial_renderer.get_partial_renderer',
280 284 'get_partial_renderer')
281 285
282 286 # Set the authorization policy.
283 287 authz_policy = ACLAuthorizationPolicy()
284 288 config.set_authorization_policy(authz_policy)
285 289
286 290 # Set the default renderer for HTML templates to mako.
287 291 config.add_mako_renderer('.html')
288 292
289 293 config.add_renderer(
290 294 name='json_ext',
291 295 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
292 296
293 297 # include RhodeCode plugins
294 298 includes = aslist(settings.get('rhodecode.includes', []))
295 299 for inc in includes:
296 300 config.include(inc)
297 301
298 302 # custom not found view, if our pyramid app doesn't know how to handle
299 303 # the request pass it to potential VCS handling ap
300 304 config.add_notfound_view(not_found_view)
301 305 if not settings.get('debugtoolbar.enabled', False):
302 306 # disabled debugtoolbar handle all exceptions via the error_handlers
303 307 config.add_view(error_handler, context=Exception)
304 308
305 309 # all errors including 403/404/50X
306 310 config.add_view(error_handler, context=HTTPError)
307 311
308 312
309 313 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
310 314 """
311 315 Apply outer WSGI middlewares around the application.
312 316 """
313 317 registry = config.registry
314 318 settings = registry.settings
315 319
316 320 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
317 321 pyramid_app = HttpsFixup(pyramid_app, settings)
318 322
319 323 pyramid_app, _ae_client = wrap_in_appenlight_if_enabled(
320 324 pyramid_app, settings)
321 325 registry.ae_client = _ae_client
322 326
323 327 if settings['gzip_responses']:
324 328 pyramid_app = make_gzip_middleware(
325 329 pyramid_app, settings, compress_level=1)
326 330
327 331 # this should be the outer most middleware in the wsgi stack since
328 332 # middleware like Routes make database calls
329 333 def pyramid_app_with_cleanup(environ, start_response):
330 334 try:
331 335 return pyramid_app(environ, start_response)
332 336 finally:
333 337 # Dispose current database session and rollback uncommitted
334 338 # transactions.
335 339 meta.Session.remove()
336 340
337 341 # In a single threaded mode server, on non sqlite db we should have
338 342 # '0 Current Checked out connections' at the end of a request,
339 343 # if not, then something, somewhere is leaving a connection open
340 344 pool = meta.Base.metadata.bind.engine.pool
341 345 log.debug('sa pool status: %s', pool.status())
342 346 log.debug('Request processing finalized')
343 347
344 348 return pyramid_app_with_cleanup
345 349
346 350
347 351 def sanitize_settings_and_apply_defaults(settings):
348 352 """
349 353 Applies settings defaults and does all type conversion.
350 354
351 355 We would move all settings parsing and preparation into this place, so that
352 356 we have only one place left which deals with this part. The remaining parts
353 357 of the application would start to rely fully on well prepared settings.
354 358
355 359 This piece would later be split up per topic to avoid a big fat monster
356 360 function.
357 361 """
358 362
359 363 settings.setdefault('rhodecode.edition', 'Community Edition')
360 364
361 365 if 'mako.default_filters' not in settings:
362 366 # set custom default filters if we don't have it defined
363 367 settings['mako.imports'] = 'from rhodecode.lib.base import h_filter'
364 368 settings['mako.default_filters'] = 'h_filter'
365 369
366 370 if 'mako.directories' not in settings:
367 371 mako_directories = settings.setdefault('mako.directories', [
368 372 # Base templates of the original application
369 373 'rhodecode:templates',
370 374 ])
371 375 log.debug(
372 376 "Using the following Mako template directories: %s",
373 377 mako_directories)
374 378
375 379 # Default includes, possible to change as a user
376 380 pyramid_includes = settings.setdefault('pyramid.includes', [
377 381 'rhodecode.lib.middleware.request_wrapper',
378 382 ])
379 383 log.debug(
380 384 "Using the following pyramid.includes: %s",
381 385 pyramid_includes)
382 386
383 387 # TODO: johbo: Re-think this, usually the call to config.include
384 388 # should allow to pass in a prefix.
385 389 settings.setdefault('rhodecode.api.url', '/_admin/api')
386 390
387 391 # Sanitize generic settings.
388 392 _list_setting(settings, 'default_encoding', 'UTF-8')
389 393 _bool_setting(settings, 'is_test', 'false')
390 394 _bool_setting(settings, 'gzip_responses', 'false')
391 395
392 396 # Call split out functions that sanitize settings for each topic.
393 397 _sanitize_appenlight_settings(settings)
394 398 _sanitize_vcs_settings(settings)
395 399 _sanitize_cache_settings(settings)
396 400
397 401 # configure instance id
398 402 config_utils.set_instance_id(settings)
399 403
400 404 return settings
401 405
402 406
403 407 def _sanitize_appenlight_settings(settings):
404 408 _bool_setting(settings, 'appenlight', 'false')
405 409
406 410
407 411 def _sanitize_vcs_settings(settings):
408 412 """
409 413 Applies settings defaults and does type conversion for all VCS related
410 414 settings.
411 415 """
412 416 _string_setting(settings, 'vcs.svn.compatible_version', '')
413 417 _string_setting(settings, 'git_rev_filter', '--all')
414 418 _string_setting(settings, 'vcs.hooks.protocol', 'http')
415 419 _string_setting(settings, 'vcs.hooks.host', '127.0.0.1')
416 420 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
417 421 _string_setting(settings, 'vcs.server', '')
418 422 _string_setting(settings, 'vcs.server.log_level', 'debug')
419 423 _string_setting(settings, 'vcs.server.protocol', 'http')
420 424 _bool_setting(settings, 'startup.import_repos', 'false')
421 425 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
422 426 _bool_setting(settings, 'vcs.server.enable', 'true')
423 427 _bool_setting(settings, 'vcs.start_server', 'false')
424 428 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
425 429 _int_setting(settings, 'vcs.connection_timeout', 3600)
426 430
427 431 # Support legacy values of vcs.scm_app_implementation. Legacy
428 432 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
429 433 # which is now mapped to 'http'.
430 434 scm_app_impl = settings['vcs.scm_app_implementation']
431 435 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
432 436 settings['vcs.scm_app_implementation'] = 'http'
433 437
434 438
435 439 def _sanitize_cache_settings(settings):
436 440 _string_setting(settings, 'cache_dir',
437 441 os.path.join(tempfile.gettempdir(), 'rc_cache'))
438 442 # cache_perms
439 443 _string_setting(
440 444 settings,
441 445 'rc_cache.cache_perms.backend',
442 446 'dogpile.cache.rc.file_namespace')
443 447 _int_setting(
444 448 settings,
445 449 'rc_cache.cache_perms.expiration_time',
446 450 60)
447 451 _string_setting(
448 452 settings,
449 453 'rc_cache.cache_perms.arguments.filename',
450 454 os.path.join(tempfile.gettempdir(), 'rc_cache_1'))
451 455
452 456 # cache_repo
453 457 _string_setting(
454 458 settings,
455 459 'rc_cache.cache_repo.backend',
456 460 'dogpile.cache.rc.file_namespace')
457 461 _int_setting(
458 462 settings,
459 463 'rc_cache.cache_repo.expiration_time',
460 464 60)
461 465 _string_setting(
462 466 settings,
463 467 'rc_cache.cache_repo.arguments.filename',
464 468 os.path.join(tempfile.gettempdir(), 'rc_cache_2'))
465 469
466 470 # sql_cache_short
467 471 _string_setting(
468 472 settings,
469 473 'rc_cache.sql_cache_short.backend',
470 474 'dogpile.cache.rc.memory_lru')
471 475 _int_setting(
472 476 settings,
473 477 'rc_cache.sql_cache_short.expiration_time',
474 478 30)
475 479 _int_setting(
476 480 settings,
477 481 'rc_cache.sql_cache_short.max_size',
478 482 10000)
479 483
480 484
481 485 def _int_setting(settings, name, default):
482 486 settings[name] = int(settings.get(name, default))
483 487
484 488
485 489 def _bool_setting(settings, name, default):
486 490 input_val = settings.get(name, default)
487 491 if isinstance(input_val, unicode):
488 492 input_val = input_val.encode('utf8')
489 493 settings[name] = asbool(input_val)
490 494
491 495
492 496 def _list_setting(settings, name, default):
493 497 raw_value = settings.get(name, default)
494 498
495 499 old_separator = ','
496 500 if old_separator in raw_value:
497 501 # If we get a comma separated list, pass it to our own function.
498 502 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
499 503 else:
500 504 # Otherwise we assume it uses pyramids space/newline separation.
501 505 settings[name] = aslist(raw_value)
502 506
503 507
504 508 def _string_setting(settings, name, default, lower=True):
505 509 value = settings.get(name, default)
506 510 if lower:
507 511 value = value.lower()
508 512 settings[name] = value
509 513
510 514
511 515 def _substitute_values(mapping, substitutions):
512 516 result = {
513 517 # Note: Cannot use regular replacements, since they would clash
514 518 # with the implementation of ConfigParser. Using "format" instead.
515 519 key: value.format(**substitutions)
516 520 for key, value in mapping.items()
517 521 }
518 522 return result
@@ -1,76 +1,78 b''
1 1 # Copyright (C) 2016-2018 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 from pyramid.threadlocal import get_current_registry
21 21 from rhodecode.events.base import RhodeCodeIntegrationEvent
22 22
23 23
24 24 log = logging.getLogger(__name__)
25 25
26 26
27 27 def trigger(event, registry=None):
28 28 """
29 29 Helper method to send an event. This wraps the pyramid logic to send an
30 30 event.
31 31 """
32 32 # For the first step we are using pyramids thread locals here. If the
33 33 # event mechanism works out as a good solution we should think about
34 34 # passing the registry as an argument to get rid of it.
35 event_name = event.__class__
36 log.debug('event %s sent for execution', event_name)
35 37 registry = registry or get_current_registry()
36 38 registry.notify(event)
37 log.debug('event %s triggered using registry %s', event.__class__, registry)
39 log.debug('event %s triggered using registry %s', event_name, registry)
38 40
39 41 # Send the events to integrations directly
40 42 from rhodecode.integrations import integrations_event_handler
41 43 if isinstance(event, RhodeCodeIntegrationEvent):
42 44 integrations_event_handler(event)
43 45
44 46
45 47 from rhodecode.events.user import ( # noqa
46 48 UserPreCreate,
47 49 UserPostCreate,
48 50 UserPreUpdate,
49 51 UserRegistered,
50 52 UserPermissionsChange,
51 53 )
52 54
53 55 from rhodecode.events.repo import ( # noqa
54 56 RepoEvent,
55 57 RepoPreCreateEvent, RepoCreateEvent,
56 58 RepoPreDeleteEvent, RepoDeleteEvent,
57 59 RepoPrePushEvent, RepoPushEvent,
58 60 RepoPrePullEvent, RepoPullEvent,
59 61 )
60 62
61 63 from rhodecode.events.repo_group import ( # noqa
62 64 RepoGroupEvent,
63 65 RepoGroupCreateEvent,
64 66 RepoGroupUpdateEvent,
65 67 RepoGroupDeleteEvent,
66 68 )
67 69
68 70 from rhodecode.events.pullrequest import ( # noqa
69 71 PullRequestEvent,
70 72 PullRequestCreateEvent,
71 73 PullRequestUpdateEvent,
72 74 PullRequestCommentEvent,
73 75 PullRequestReviewEvent,
74 76 PullRequestMergeEvent,
75 77 PullRequestCloseEvent,
76 78 )
@@ -1,264 +1,279 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 from rhodecode.lib.jsonalchemy import JsonRaw
25 25 from rhodecode.model import meta
26 26 from rhodecode.model.db import User, UserLog, Repository
27 27
28 28
29 29 log = logging.getLogger(__name__)
30 30
31 31 # action as key, and expected action_data as value
32 32 ACTIONS_V1 = {
33 33 'user.login.success': {'user_agent': ''},
34 34 'user.login.failure': {'user_agent': ''},
35 35 'user.logout': {'user_agent': ''},
36 36 'user.register': {},
37 37 'user.password.reset_request': {},
38 38 'user.push': {'user_agent': '', 'commit_ids': []},
39 39 'user.pull': {'user_agent': ''},
40 40
41 41 'user.create': {'data': {}},
42 42 'user.delete': {'old_data': {}},
43 43 'user.edit': {'old_data': {}},
44 44 'user.edit.permissions': {},
45 45 'user.edit.ip.add': {'ip': {}, 'user': {}},
46 46 'user.edit.ip.delete': {'ip': {}, 'user': {}},
47 47 'user.edit.token.add': {'token': {}, 'user': {}},
48 48 'user.edit.token.delete': {'token': {}, 'user': {}},
49 49 'user.edit.email.add': {'email': ''},
50 50 'user.edit.email.delete': {'email': ''},
51 51 'user.edit.ssh_key.add': {'token': {}, 'user': {}},
52 52 'user.edit.ssh_key.delete': {'token': {}, 'user': {}},
53 53 'user.edit.password_reset.enabled': {},
54 54 'user.edit.password_reset.disabled': {},
55 55
56 56 'user_group.create': {'data': {}},
57 57 'user_group.delete': {'old_data': {}},
58 58 'user_group.edit': {'old_data': {}},
59 59 'user_group.edit.permissions': {},
60 60 'user_group.edit.member.add': {'user': {}},
61 61 'user_group.edit.member.delete': {'user': {}},
62 62
63 63 'repo.create': {'data': {}},
64 64 'repo.fork': {'data': {}},
65 65 'repo.edit': {'old_data': {}},
66 66 'repo.edit.permissions': {},
67 67 'repo.delete': {'old_data': {}},
68 68 'repo.commit.strip': {'commit_id': ''},
69 69 'repo.archive.download': {'user_agent': '', 'archive_name': '',
70 70 'archive_spec': '', 'archive_cached': ''},
71 71 'repo.pull_request.create': '',
72 72 'repo.pull_request.edit': '',
73 73 'repo.pull_request.delete': '',
74 74 'repo.pull_request.close': '',
75 75 'repo.pull_request.merge': '',
76 76 'repo.pull_request.vote': '',
77 77 'repo.pull_request.comment.create': '',
78 78 'repo.pull_request.comment.delete': '',
79 79
80 80 'repo.pull_request.reviewer.add': '',
81 81 'repo.pull_request.reviewer.delete': '',
82 82
83 83 'repo.commit.comment.create': {'data': {}},
84 84 'repo.commit.comment.delete': {'data': {}},
85 85 'repo.commit.vote': '',
86 86
87 87 'repo_group.create': {'data': {}},
88 88 'repo_group.edit': {'old_data': {}},
89 89 'repo_group.edit.permissions': {},
90 90 'repo_group.delete': {'old_data': {}},
91 91 }
92 92 ACTIONS = ACTIONS_V1
93 93
94 94 SOURCE_WEB = 'source_web'
95 95 SOURCE_API = 'source_api'
96 96
97 97
98 98 class UserWrap(object):
99 99 """
100 100 Fake object used to imitate AuthUser
101 101 """
102 102
103 103 def __init__(self, user_id=None, username=None, ip_addr=None):
104 104 self.user_id = user_id
105 105 self.username = username
106 106 self.ip_addr = ip_addr
107 107
108 108
109 109 class RepoWrap(object):
110 110 """
111 111 Fake object used to imitate RepoObject that audit logger requires
112 112 """
113 113
114 114 def __init__(self, repo_id=None, repo_name=None):
115 115 self.repo_id = repo_id
116 116 self.repo_name = repo_name
117 117
118 118
119 119 def _store_log(action_name, action_data, user_id, username, user_data,
120 120 ip_address, repository_id, repository_name):
121 121 user_log = UserLog()
122 122 user_log.version = UserLog.VERSION_2
123 123
124 124 user_log.action = action_name
125 125 user_log.action_data = action_data or JsonRaw(u'{}')
126 126
127 127 user_log.user_ip = ip_address
128 128
129 129 user_log.user_id = user_id
130 130 user_log.username = username
131 131 user_log.user_data = user_data or JsonRaw(u'{}')
132 132
133 133 user_log.repository_id = repository_id
134 134 user_log.repository_name = repository_name
135 135
136 136 user_log.action_date = datetime.datetime.now()
137 137
138 138 return user_log
139 139
140 140
141 141 def store_web(*args, **kwargs):
142 142 if 'action_data' not in kwargs:
143 143 kwargs['action_data'] = {}
144 144 kwargs['action_data'].update({
145 145 'source': SOURCE_WEB
146 146 })
147 147 return store(*args, **kwargs)
148 148
149 149
150 150 def store_api(*args, **kwargs):
151 151 if 'action_data' not in kwargs:
152 152 kwargs['action_data'] = {}
153 153 kwargs['action_data'].update({
154 154 'source': SOURCE_API
155 155 })
156 156 return store(*args, **kwargs)
157 157
158 158
159 159 def store(action, user, action_data=None, user_data=None, ip_addr=None,
160 160 repo=None, sa_session=None, commit=False):
161 161 """
162 162 Audit logger for various actions made by users, typically this
163 163 results in a call such::
164 164
165 165 from rhodecode.lib import audit_logger
166 166
167 167 audit_logger.store(
168 168 'repo.edit', user=self._rhodecode_user)
169 169 audit_logger.store(
170 170 'repo.delete', action_data={'data': repo_data},
171 171 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'))
172 172
173 173 # repo action
174 174 audit_logger.store(
175 175 'repo.delete',
176 176 user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8'),
177 177 repo=audit_logger.RepoWrap(repo_name='some-repo'))
178 178
179 179 # repo action, when we know and have the repository object already
180 180 audit_logger.store(
181 181 'repo.delete', action_data={'source': audit_logger.SOURCE_WEB, },
182 182 user=self._rhodecode_user,
183 183 repo=repo_object)
184 184
185 185 # alternative wrapper to the above
186 186 audit_logger.store_web(
187 187 'repo.delete', action_data={},
188 188 user=self._rhodecode_user,
189 189 repo=repo_object)
190 190
191 191 # without an user ?
192 192 audit_logger.store(
193 193 'user.login.failure',
194 194 user=audit_logger.UserWrap(
195 195 username=self.request.params.get('username'),
196 196 ip_addr=self.request.remote_addr))
197 197
198 198 """
199 199 from rhodecode.lib.utils2 import safe_unicode
200 200 from rhodecode.lib.auth import AuthUser
201 201
202 202 action_spec = ACTIONS.get(action, None)
203 203 if action_spec is None:
204 204 raise ValueError('Action `{}` is not supported'.format(action))
205 205
206 206 if not sa_session:
207 207 sa_session = meta.Session()
208 208
209 209 try:
210 210 username = getattr(user, 'username', None)
211 211 if not username:
212 212 pass
213 213
214 214 user_id = getattr(user, 'user_id', None)
215 215 if not user_id:
216 216 # maybe we have username ? Try to figure user_id from username
217 217 if username:
218 218 user_id = getattr(
219 219 User.get_by_username(username), 'user_id', None)
220 220
221 221 ip_addr = ip_addr or getattr(user, 'ip_addr', None)
222 222 if not ip_addr:
223 223 pass
224 224
225 225 if not user_data:
226 226 # try to get this from the auth user
227 227 if isinstance(user, AuthUser):
228 228 user_data = {
229 229 'username': user.username,
230 230 'email': user.email,
231 231 }
232 232
233 233 repository_name = getattr(repo, 'repo_name', None)
234 234 repository_id = getattr(repo, 'repo_id', None)
235 235 if not repository_id:
236 236 # maybe we have repo_name ? Try to figure repo_id from repo_name
237 237 if repository_name:
238 238 repository_id = getattr(
239 239 Repository.get_by_repo_name(repository_name), 'repo_id', None)
240 240
241 241 action_name = safe_unicode(action)
242 242 ip_address = safe_unicode(ip_addr)
243 243
244 with sa_session.no_autoflush:
245 update_user_last_activity(sa_session, user_id)
246
244 247 user_log = _store_log(
245 248 action_name=action_name,
246 249 action_data=action_data or {},
247 250 user_id=user_id,
248 251 username=username,
249 252 user_data=user_data or {},
250 253 ip_address=ip_address,
251 254 repository_id=repository_id,
252 255 repository_name=repository_name
253 256 )
254 257
255 258 sa_session.add(user_log)
259
256 260 if commit:
257 261 sa_session.commit()
258 262
259 263 entry_id = user_log.entry_id or ''
260 264 log.info('AUDIT[%s]: Logging action: `%s` by user:id:%s[%s] ip:%s',
261 265 entry_id, action_name, user_id, username, ip_address)
262 266
263 267 except Exception:
264 268 log.exception('AUDIT: failed to store audit log')
269
270
271 def update_user_last_activity(sa_session, user_id):
272 _last_activity = datetime.datetime.now()
273 try:
274 sa_session.query(User).filter(User.user_id == user_id).update(
275 {"last_activity": _last_activity})
276 log.debug(
277 'updated user `%s` last activity to:%s', user_id, _last_activity)
278 except Exception:
279 log.exception("Failed last activity update")
@@ -1,659 +1,661 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31 from StringIO import StringIO
32 32 from lxml import etree
33 33
34 34 import time
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from pyramid.httpexceptions import (
38 38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 39 from zope.cachedescriptors.property import Lazy as LazyProperty
40 40
41 41 import rhodecode
42 42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 43 from rhodecode.lib import caches, rc_cache
44 44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 45 from rhodecode.lib.base import (
46 46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 49 from rhodecode.lib.middleware import appenlight
50 50 from rhodecode.lib.middleware.utils import scm_app_http
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def extract_svn_txn_id(acl_repo_name, data):
66 66 """
67 67 Helper method for extraction of svn txn_id from submited XML data during
68 68 POST operations
69 69 """
70 70 try:
71 71 root = etree.fromstring(data)
72 72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 73 for el in root:
74 74 if el.tag == '{DAV:}source':
75 75 for sub_el in el:
76 76 if sub_el.tag == '{DAV:}href':
77 77 match = pat.search(sub_el.text)
78 78 if match:
79 79 svn_tx_id = match.groupdict()['txn_id']
80 80 txn_id = caches.compute_key_from_params(
81 81 acl_repo_name, svn_tx_id)
82 82 return txn_id
83 83 except Exception:
84 84 log.exception('Failed to extract txn_id')
85 85
86 86
87 87 def initialize_generator(factory):
88 88 """
89 89 Initializes the returned generator by draining its first element.
90 90
91 91 This can be used to give a generator an initializer, which is the code
92 92 up to the first yield statement. This decorator enforces that the first
93 93 produced element has the value ``"__init__"`` to make its special
94 94 purpose very explicit in the using code.
95 95 """
96 96
97 97 @wraps(factory)
98 98 def wrapper(*args, **kwargs):
99 99 gen = factory(*args, **kwargs)
100 100 try:
101 101 init = gen.next()
102 102 except StopIteration:
103 103 raise ValueError('Generator must yield at least one element.')
104 104 if init != "__init__":
105 105 raise ValueError('First yielded element must be "__init__".')
106 106 return gen
107 107 return wrapper
108 108
109 109
110 110 class SimpleVCS(object):
111 111 """Common functionality for SCM HTTP handlers."""
112 112
113 113 SCM = 'unknown'
114 114
115 115 acl_repo_name = None
116 116 url_repo_name = None
117 117 vcs_repo_name = None
118 118 rc_extras = {}
119 119
120 120 # We have to handle requests to shadow repositories different than requests
121 121 # to normal repositories. Therefore we have to distinguish them. To do this
122 122 # we use this regex which will match only on URLs pointing to shadow
123 123 # repositories.
124 124 shadow_repo_re = re.compile(
125 125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 126 '(?P<target>{slug_pat})/' # target repo
127 127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 128 'repository$' # shadow repo
129 129 .format(slug_pat=SLUG_RE.pattern))
130 130
131 131 def __init__(self, config, registry):
132 132 self.registry = registry
133 133 self.config = config
134 134 # re-populated by specialized middleware
135 135 self.repo_vcs_config = base.Config()
136 136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137 137
138 138 registry.rhodecode_settings = self.rhodecode_settings
139 139 # authenticate this VCS request using authfunc
140 140 auth_ret_code_detection = \
141 141 str2bool(self.config.get('auth_ret_code_detection', False))
142 142 self.authenticate = BasicAuth(
143 143 '', authenticate, registry, config.get('auth_ret_code'),
144 144 auth_ret_code_detection)
145 145 self.ip_addr = '0.0.0.0'
146 146
147 147 @LazyProperty
148 148 def global_vcs_config(self):
149 149 try:
150 150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 151 except Exception:
152 152 return base.Config()
153 153
154 154 @property
155 155 def base_path(self):
156 156 settings_path = self.repo_vcs_config.get(
157 157 *VcsSettingsModel.PATH_SETTING)
158 158
159 159 if not settings_path:
160 160 settings_path = self.global_vcs_config.get(
161 161 *VcsSettingsModel.PATH_SETTING)
162 162
163 163 if not settings_path:
164 164 # try, maybe we passed in explicitly as config option
165 165 settings_path = self.config.get('base_path')
166 166
167 167 if not settings_path:
168 168 raise ValueError('FATAL: base_path is empty')
169 169 return settings_path
170 170
171 171 def set_repo_names(self, environ):
172 172 """
173 173 This will populate the attributes acl_repo_name, url_repo_name,
174 174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 175 shadow) repositories all names are equal. In case of requests to a
176 176 shadow repository the acl-name points to the target repo of the pull
177 177 request and the vcs-name points to the shadow repo file system path.
178 178 The url-name is always the URL used by the vcs client program.
179 179
180 180 Example in case of a shadow repo:
181 181 acl_repo_name = RepoGroup/MyRepo
182 182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 184 """
185 185 # First we set the repo name from URL for all attributes. This is the
186 186 # default if handling normal (non shadow) repo requests.
187 187 self.url_repo_name = self._get_repository_name(environ)
188 188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 189 self.is_shadow_repo = False
190 190
191 191 # Check if this is a request to a shadow repository.
192 192 match = self.shadow_repo_re.match(self.url_repo_name)
193 193 if match:
194 194 match_dict = match.groupdict()
195 195
196 196 # Build acl repo name from regex match.
197 197 acl_repo_name = safe_unicode('{groups}{target}'.format(
198 198 groups=match_dict['groups'] or '',
199 199 target=match_dict['target']))
200 200
201 201 # Retrieve pull request instance by ID from regex match.
202 202 pull_request = PullRequest.get(match_dict['pr_id'])
203 203
204 204 # Only proceed if we got a pull request and if acl repo name from
205 205 # URL equals the target repo name of the pull request.
206 206 if pull_request and \
207 207 (acl_repo_name == pull_request.target_repo.repo_name):
208 208 repo_id = pull_request.target_repo.repo_id
209 209 # Get file system path to shadow repository.
210 210 workspace_id = PullRequestModel()._workspace_id(pull_request)
211 211 target_vcs = pull_request.target_repo.scm_instance()
212 212 vcs_repo_name = target_vcs._get_shadow_repository_path(
213 213 repo_id, workspace_id)
214 214
215 215 # Store names for later usage.
216 216 self.vcs_repo_name = vcs_repo_name
217 217 self.acl_repo_name = acl_repo_name
218 218 self.is_shadow_repo = True
219 219
220 220 log.debug('Setting all VCS repository names: %s', {
221 221 'acl_repo_name': self.acl_repo_name,
222 222 'url_repo_name': self.url_repo_name,
223 223 'vcs_repo_name': self.vcs_repo_name,
224 224 })
225 225
226 226 @property
227 227 def scm_app(self):
228 228 custom_implementation = self.config['vcs.scm_app_implementation']
229 229 if custom_implementation == 'http':
230 230 log.info('Using HTTP implementation of scm app.')
231 231 scm_app_impl = scm_app_http
232 232 else:
233 233 log.info('Using custom implementation of scm_app: "{}"'.format(
234 234 custom_implementation))
235 235 scm_app_impl = importlib.import_module(custom_implementation)
236 236 return scm_app_impl
237 237
238 238 def _get_by_id(self, repo_name):
239 239 """
240 240 Gets a special pattern _<ID> from clone url and tries to replace it
241 241 with a repository_name for support of _<ID> non changeable urls
242 242 """
243 243
244 244 data = repo_name.split('/')
245 245 if len(data) >= 2:
246 246 from rhodecode.model.repo import RepoModel
247 247 by_id_match = RepoModel().get_repo_by_id(repo_name)
248 248 if by_id_match:
249 249 data[1] = by_id_match.repo_name
250 250
251 251 return safe_str('/'.join(data))
252 252
253 253 def _invalidate_cache(self, repo_name):
254 254 """
255 255 Set's cache for this repository for invalidation on next access
256 256
257 257 :param repo_name: full repo name, also a cache key
258 258 """
259 259 ScmModel().mark_for_invalidation(repo_name)
260 260
261 261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 262 db_repo = Repository.get_by_repo_name(repo_name)
263 263 if not db_repo:
264 264 log.debug('Repository `%s` not found inside the database.',
265 265 repo_name)
266 266 return False
267 267
268 268 if db_repo.repo_type != scm_type:
269 269 log.warning(
270 270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 271 repo_name, db_repo.repo_type, scm_type)
272 272 return False
273 273
274 274 config = db_repo._config
275 275 config.set('extensions', 'largefiles', '')
276 276 return is_valid_repo(
277 277 repo_name, base_path,
278 278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279 279
280 280 def valid_and_active_user(self, user):
281 281 """
282 282 Checks if that user is not empty, and if it's actually object it checks
283 283 if he's active.
284 284
285 285 :param user: user object or None
286 286 :return: boolean
287 287 """
288 288 if user is None:
289 289 return False
290 290
291 291 elif user.active:
292 292 return True
293 293
294 294 return False
295 295
296 296 @property
297 297 def is_shadow_repo_dir(self):
298 298 return os.path.isdir(self.vcs_repo_name)
299 299
300 300 def _check_permission(self, action, user, repo_name, ip_addr=None,
301 301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 302 """
303 303 Checks permissions using action (push/pull) user and repository
304 304 name. If plugin_cache and ttl is set it will use the plugin which
305 305 authenticated the user to store the cached permissions result for N
306 306 amount of seconds as in cache_ttl
307 307
308 308 :param action: push or pull action
309 309 :param user: user instance
310 310 :param repo_name: repository name
311 311 """
312 312
313 313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 314 plugin_id, plugin_cache_active, cache_ttl)
315 315
316 316 user_id = user.user_id
317 317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319 319
320 320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 321 expiration_time=cache_ttl,
322 322 condition=plugin_cache_active)
323 323 def compute_perm_vcs(
324 324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325 325
326 326 log.debug('auth: calculating permission access now...')
327 327 # check IP
328 328 inherit = user.inherit_default_permissions
329 329 ip_allowed = AuthUser.check_ip_allowed(
330 330 user_id, ip_addr, inherit_from_default=inherit)
331 331 if ip_allowed:
332 332 log.info('Access for IP:%s allowed', ip_addr)
333 333 else:
334 334 return False
335 335
336 336 if action == 'push':
337 337 perms = ('repository.write', 'repository.admin')
338 338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
339 339 return False
340 340
341 341 else:
342 342 # any other action need at least read permission
343 343 perms = (
344 344 'repository.read', 'repository.write', 'repository.admin')
345 345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
346 346 return False
347 347
348 348 return True
349 349
350 350 start = time.time()
351 351 log.debug('Running plugin `%s` permissions check', plugin_id)
352 352
353 353 # for environ based auth, password can be empty, but then the validation is
354 354 # on the server that fills in the env data needed for authentication
355 355 perm_result = compute_perm_vcs(
356 356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357 357
358 358 auth_time = time.time() - start
359 359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
360 360 'expiration time of fetched cache %.1fs.',
361 361 plugin_id, auth_time, cache_ttl)
362 362
363 363 return perm_result
364 364
365 365 def _check_ssl(self, environ, start_response):
366 366 """
367 367 Checks the SSL check flag and returns False if SSL is not present
368 368 and required True otherwise
369 369 """
370 370 org_proto = environ['wsgi._org_proto']
371 371 # check if we have SSL required ! if not it's a bad request !
372 372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
373 373 if require_ssl and org_proto == 'http':
374 374 log.debug(
375 375 'Bad request: detected protocol is `%s` and '
376 376 'SSL/HTTPS is required.', org_proto)
377 377 return False
378 378 return True
379 379
380 380 def _get_default_cache_ttl(self):
381 381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
382 382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
383 383 plugin_settings = plugin.get_settings()
384 384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
385 385 plugin_settings) or (False, 0)
386 386 return plugin_cache_active, cache_ttl
387 387
388 388 def __call__(self, environ, start_response):
389 389 try:
390 390 return self._handle_request(environ, start_response)
391 391 except Exception:
392 392 log.exception("Exception while handling request")
393 393 appenlight.track_exception(environ)
394 394 return HTTPInternalServerError()(environ, start_response)
395 395 finally:
396 396 meta.Session.remove()
397 397
398 398 def _handle_request(self, environ, start_response):
399 399
400 400 if not self._check_ssl(environ, start_response):
401 401 reason = ('SSL required, while RhodeCode was unable '
402 402 'to detect this as SSL request')
403 403 log.debug('User not allowed to proceed, %s', reason)
404 404 return HTTPNotAcceptable(reason)(environ, start_response)
405 405
406 406 if not self.url_repo_name:
407 407 log.warning('Repository name is empty: %s', self.url_repo_name)
408 408 # failed to get repo name, we fail now
409 409 return HTTPNotFound()(environ, start_response)
410 410 log.debug('Extracted repo name is %s', self.url_repo_name)
411 411
412 412 ip_addr = get_ip_addr(environ)
413 413 user_agent = get_user_agent(environ)
414 414 username = None
415 415
416 416 # skip passing error to error controller
417 417 environ['pylons.status_code_redirect'] = True
418 418
419 419 # ======================================================================
420 420 # GET ACTION PULL or PUSH
421 421 # ======================================================================
422 422 action = self._get_action(environ)
423 423
424 424 # ======================================================================
425 425 # Check if this is a request to a shadow repository of a pull request.
426 426 # In this case only pull action is allowed.
427 427 # ======================================================================
428 428 if self.is_shadow_repo and action != 'pull':
429 429 reason = 'Only pull action is allowed for shadow repositories.'
430 430 log.debug('User not allowed to proceed, %s', reason)
431 431 return HTTPNotAcceptable(reason)(environ, start_response)
432 432
433 433 # Check if the shadow repo actually exists, in case someone refers
434 434 # to it, and it has been deleted because of successful merge.
435 435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
436 436 log.debug(
437 437 'Shadow repo detected, and shadow repo dir `%s` is missing',
438 438 self.is_shadow_repo_dir)
439 439 return HTTPNotFound()(environ, start_response)
440 440
441 441 # ======================================================================
442 442 # CHECK ANONYMOUS PERMISSION
443 443 # ======================================================================
444 444 if action in ['pull', 'push']:
445 445 anonymous_user = User.get_default_user()
446 446 username = anonymous_user.username
447 447 if anonymous_user.active:
448 448 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
449 449 # ONLY check permissions if the user is activated
450 450 anonymous_perm = self._check_permission(
451 451 action, anonymous_user, self.acl_repo_name, ip_addr,
452 452 plugin_id='anonymous_access',
453 453 plugin_cache_active=plugin_cache_active,
454 454 cache_ttl=cache_ttl,
455 455 )
456 456 else:
457 457 anonymous_perm = False
458 458
459 459 if not anonymous_user.active or not anonymous_perm:
460 460 if not anonymous_user.active:
461 461 log.debug('Anonymous access is disabled, running '
462 462 'authentication')
463 463
464 464 if not anonymous_perm:
465 465 log.debug('Not enough credentials to access this '
466 466 'repository as anonymous user')
467 467
468 468 username = None
469 469 # ==============================================================
470 470 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
471 471 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
472 472 # ==============================================================
473 473
474 474 # try to auth based on environ, container auth methods
475 475 log.debug('Running PRE-AUTH for container based authentication')
476 476 pre_auth = authenticate(
477 477 '', '', environ, VCS_TYPE, registry=self.registry,
478 478 acl_repo_name=self.acl_repo_name)
479 479 if pre_auth and pre_auth.get('username'):
480 480 username = pre_auth['username']
481 481 log.debug('PRE-AUTH got %s as username', username)
482 482 if pre_auth:
483 483 log.debug('PRE-AUTH successful from %s',
484 484 pre_auth.get('auth_data', {}).get('_plugin'))
485 485
486 486 # If not authenticated by the container, running basic auth
487 487 # before inject the calling repo_name for special scope checks
488 488 self.authenticate.acl_repo_name = self.acl_repo_name
489 489
490 490 plugin_cache_active, cache_ttl = False, 0
491 491 plugin = None
492 492 if not username:
493 493 self.authenticate.realm = self.authenticate.get_rc_realm()
494 494
495 495 try:
496 496 auth_result = self.authenticate(environ)
497 497 except (UserCreationError, NotAllowedToCreateUserError) as e:
498 498 log.error(e)
499 499 reason = safe_str(e)
500 500 return HTTPNotAcceptable(reason)(environ, start_response)
501 501
502 502 if isinstance(auth_result, dict):
503 503 AUTH_TYPE.update(environ, 'basic')
504 504 REMOTE_USER.update(environ, auth_result['username'])
505 505 username = auth_result['username']
506 506 plugin = auth_result.get('auth_data', {}).get('_plugin')
507 507 log.info(
508 508 'MAIN-AUTH successful for user `%s` from %s plugin',
509 509 username, plugin)
510 510
511 511 plugin_cache_active, cache_ttl = auth_result.get(
512 512 'auth_data', {}).get('_ttl_cache') or (False, 0)
513 513 else:
514 514 return auth_result.wsgi_application(
515 515 environ, start_response)
516 516
517 517 # ==============================================================
518 518 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
519 519 # ==============================================================
520 520 user = User.get_by_username(username)
521 521 if not self.valid_and_active_user(user):
522 522 return HTTPForbidden()(environ, start_response)
523 523 username = user.username
524 user_id = user.user_id
524 525
525 526 # check user attributes for password change flag
526 527 user_obj = user
527 528 if user_obj and user_obj.username != User.DEFAULT_USER and \
528 529 user_obj.user_data.get('force_password_change'):
529 530 reason = 'password change required'
530 531 log.debug('User not allowed to authenticate, %s', reason)
531 532 return HTTPNotAcceptable(reason)(environ, start_response)
532 533
533 534 # check permissions for this repository
534 535 perm = self._check_permission(
535 536 action, user, self.acl_repo_name, ip_addr,
536 537 plugin, plugin_cache_active, cache_ttl)
537 538 if not perm:
538 539 return HTTPForbidden()(environ, start_response)
540 environ['rc_auth_user_id'] = user_id
539 541
540 542 # extras are injected into UI object and later available
541 543 # in hooks executed by RhodeCode
542 544 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
543 545 extras = vcs_operation_context(
544 546 environ, repo_name=self.acl_repo_name, username=username,
545 547 action=action, scm=self.SCM, check_locking=check_locking,
546 548 is_shadow_repo=self.is_shadow_repo
547 549 )
548 550
549 551 # ======================================================================
550 552 # REQUEST HANDLING
551 553 # ======================================================================
552 554 repo_path = os.path.join(
553 555 safe_str(self.base_path), safe_str(self.vcs_repo_name))
554 556 log.debug('Repository path is %s', repo_path)
555 557
556 558 fix_PATH()
557 559
558 560 log.info(
559 561 '%s action on %s repo "%s" by "%s" from %s %s',
560 562 action, self.SCM, safe_str(self.url_repo_name),
561 563 safe_str(username), ip_addr, user_agent)
562 564
563 565 return self._generate_vcs_response(
564 566 environ, start_response, repo_path, extras, action)
565 567
566 568 @initialize_generator
567 569 def _generate_vcs_response(
568 570 self, environ, start_response, repo_path, extras, action):
569 571 """
570 572 Returns a generator for the response content.
571 573
572 574 This method is implemented as a generator, so that it can trigger
573 575 the cache validation after all content sent back to the client. It
574 576 also handles the locking exceptions which will be triggered when
575 577 the first chunk is produced by the underlying WSGI application.
576 578 """
577 579 txn_id = ''
578 580 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
579 581 # case for SVN, we want to re-use the callback daemon port
580 582 # so we use the txn_id, for this we peek the body, and still save
581 583 # it as wsgi.input
582 584 data = environ['wsgi.input'].read()
583 585 environ['wsgi.input'] = StringIO(data)
584 586 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
585 587
586 588 callback_daemon, extras = self._prepare_callback_daemon(
587 589 extras, environ, action, txn_id=txn_id)
588 590 log.debug('HOOKS extras is %s', extras)
589 591
590 592 config = self._create_config(extras, self.acl_repo_name)
591 593 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
592 594 with callback_daemon:
593 595 app.rc_extras = extras
594 596
595 597 try:
596 598 response = app(environ, start_response)
597 599 finally:
598 600 # This statement works together with the decorator
599 601 # "initialize_generator" above. The decorator ensures that
600 602 # we hit the first yield statement before the generator is
601 603 # returned back to the WSGI server. This is needed to
602 604 # ensure that the call to "app" above triggers the
603 605 # needed callback to "start_response" before the
604 606 # generator is actually used.
605 607 yield "__init__"
606 608
607 609 # iter content
608 610 for chunk in response:
609 611 yield chunk
610 612
611 613 try:
612 614 # invalidate cache on push
613 615 if action == 'push':
614 616 self._invalidate_cache(self.url_repo_name)
615 617 finally:
616 618 meta.Session.remove()
617 619
618 620 def _get_repository_name(self, environ):
619 621 """Get repository name out of the environmnent
620 622
621 623 :param environ: WSGI environment
622 624 """
623 625 raise NotImplementedError()
624 626
625 627 def _get_action(self, environ):
626 628 """Map request commands into a pull or push command.
627 629
628 630 :param environ: WSGI environment
629 631 """
630 632 raise NotImplementedError()
631 633
632 634 def _create_wsgi_app(self, repo_path, repo_name, config):
633 635 """Return the WSGI app that will finally handle the request."""
634 636 raise NotImplementedError()
635 637
636 638 def _create_config(self, extras, repo_name):
637 639 """Create a safe config representation."""
638 640 raise NotImplementedError()
639 641
640 642 def _should_use_callback_daemon(self, extras, environ, action):
641 643 return True
642 644
643 645 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
644 646 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
645 647 if not self._should_use_callback_daemon(extras, environ, action):
646 648 # disable callback daemon for actions that don't require it
647 649 direct_calls = True
648 650
649 651 return prepare_callback_daemon(
650 652 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
651 653 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
652 654
653 655
654 656 def _should_check_locking(query_string):
655 657 # this is kind of hacky, but due to how mercurial handles client-server
656 658 # server see all operation on commit; bookmarks, phases and
657 659 # obsolescence marker in different transaction, we don't want to check
658 660 # locking on those
659 661 return query_string not in ['cmd=listkeys']
@@ -1,4540 +1,4534 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37 from sqlalchemy import (
38 38 or_, and_, not_, func, TypeDecorator, event,
39 39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 41 Text, Float, PickleType)
42 42 from sqlalchemy.sql.expression import true, false
43 43 from sqlalchemy.sql.functions import coalesce, count # noqa
44 44 from sqlalchemy.orm import (
45 45 relationship, joinedload, class_mapper, validates, aliased)
46 46 from sqlalchemy.ext.declarative import declared_attr
47 47 from sqlalchemy.ext.hybrid import hybrid_property
48 48 from sqlalchemy.exc import IntegrityError # noqa
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from beaker.cache import cache_region
51 51 from zope.cachedescriptors.property import Lazy as LazyProperty
52 52
53 53 from pyramid.threadlocal import get_current_request
54 54
55 55 from rhodecode.translation import _
56 56 from rhodecode.lib.vcs import get_vcs_instance
57 57 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
58 58 from rhodecode.lib.utils2 import (
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re, StrictAttributeDict, cleaned_uri)
62 62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
63 63 JsonRaw
64 64 from rhodecode.lib.ext_json import json
65 65 from rhodecode.lib.caching_query import FromCache
66 66 from rhodecode.lib.encrypt import AESCipher
67 67
68 68 from rhodecode.model.meta import Base, Session
69 69
70 70 URL_SEP = '/'
71 71 log = logging.getLogger(__name__)
72 72
73 73 # =============================================================================
74 74 # BASE CLASSES
75 75 # =============================================================================
76 76
77 77 # this is propagated from .ini file rhodecode.encrypted_values.secret or
78 78 # beaker.session.secret if first is not set.
79 79 # and initialized at environment.py
80 80 ENCRYPTION_KEY = None
81 81
82 82 # used to sort permissions by types, '#' used here is not allowed to be in
83 83 # usernames, and it's very early in sorted string.printable table.
84 84 PERMISSION_TYPE_SORT = {
85 85 'admin': '####',
86 86 'write': '###',
87 87 'read': '##',
88 88 'none': '#',
89 89 }
90 90
91 91
92 92 def display_user_sort(obj):
93 93 """
94 94 Sort function used to sort permissions in .permissions() function of
95 95 Repository, RepoGroup, UserGroup. Also it put the default user in front
96 96 of all other resources
97 97 """
98 98
99 99 if obj.username == User.DEFAULT_USER:
100 100 return '#####'
101 101 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
102 102 return prefix + obj.username
103 103
104 104
105 105 def display_user_group_sort(obj):
106 106 """
107 107 Sort function used to sort permissions in .permissions() function of
108 108 Repository, RepoGroup, UserGroup. Also it put the default user in front
109 109 of all other resources
110 110 """
111 111
112 112 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
113 113 return prefix + obj.users_group_name
114 114
115 115
116 116 def _hash_key(k):
117 117 return sha1_safe(k)
118 118
119 119
120 120 def in_filter_generator(qry, items, limit=500):
121 121 """
122 122 Splits IN() into multiple with OR
123 123 e.g.::
124 124 cnt = Repository.query().filter(
125 125 or_(
126 126 *in_filter_generator(Repository.repo_id, range(100000))
127 127 )).count()
128 128 """
129 129 if not items:
130 130 # empty list will cause empty query which might cause security issues
131 131 # this can lead to hidden unpleasant results
132 132 items = [-1]
133 133
134 134 parts = []
135 135 for chunk in xrange(0, len(items), limit):
136 136 parts.append(
137 137 qry.in_(items[chunk: chunk + limit])
138 138 )
139 139
140 140 return parts
141 141
142 142
143 143 base_table_args = {
144 144 'extend_existing': True,
145 145 'mysql_engine': 'InnoDB',
146 146 'mysql_charset': 'utf8',
147 147 'sqlite_autoincrement': True
148 148 }
149 149
150 150
151 151 class EncryptedTextValue(TypeDecorator):
152 152 """
153 153 Special column for encrypted long text data, use like::
154 154
155 155 value = Column("encrypted_value", EncryptedValue(), nullable=False)
156 156
157 157 This column is intelligent so if value is in unencrypted form it return
158 158 unencrypted form, but on save it always encrypts
159 159 """
160 160 impl = Text
161 161
162 162 def process_bind_param(self, value, dialect):
163 163 if not value:
164 164 return value
165 165 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
166 166 # protect against double encrypting if someone manually starts
167 167 # doing
168 168 raise ValueError('value needs to be in unencrypted format, ie. '
169 169 'not starting with enc$aes')
170 170 return 'enc$aes_hmac$%s' % AESCipher(
171 171 ENCRYPTION_KEY, hmac=True).encrypt(value)
172 172
173 173 def process_result_value(self, value, dialect):
174 174 import rhodecode
175 175
176 176 if not value:
177 177 return value
178 178
179 179 parts = value.split('$', 3)
180 180 if not len(parts) == 3:
181 181 # probably not encrypted values
182 182 return value
183 183 else:
184 184 if parts[0] != 'enc':
185 185 # parts ok but without our header ?
186 186 return value
187 187 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
188 188 'rhodecode.encrypted_values.strict') or True)
189 189 # at that stage we know it's our encryption
190 190 if parts[1] == 'aes':
191 191 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
192 192 elif parts[1] == 'aes_hmac':
193 193 decrypted_data = AESCipher(
194 194 ENCRYPTION_KEY, hmac=True,
195 195 strict_verification=enc_strict_mode).decrypt(parts[2])
196 196 else:
197 197 raise ValueError(
198 198 'Encryption type part is wrong, must be `aes` '
199 199 'or `aes_hmac`, got `%s` instead' % (parts[1]))
200 200 return decrypted_data
201 201
202 202
203 203 class BaseModel(object):
204 204 """
205 205 Base Model for all classes
206 206 """
207 207
208 208 @classmethod
209 209 def _get_keys(cls):
210 210 """return column names for this model """
211 211 return class_mapper(cls).c.keys()
212 212
213 213 def get_dict(self):
214 214 """
215 215 return dict with keys and values corresponding
216 216 to this model data """
217 217
218 218 d = {}
219 219 for k in self._get_keys():
220 220 d[k] = getattr(self, k)
221 221
222 222 # also use __json__() if present to get additional fields
223 223 _json_attr = getattr(self, '__json__', None)
224 224 if _json_attr:
225 225 # update with attributes from __json__
226 226 if callable(_json_attr):
227 227 _json_attr = _json_attr()
228 228 for k, val in _json_attr.iteritems():
229 229 d[k] = val
230 230 return d
231 231
232 232 def get_appstruct(self):
233 233 """return list with keys and values tuples corresponding
234 234 to this model data """
235 235
236 236 lst = []
237 237 for k in self._get_keys():
238 238 lst.append((k, getattr(self, k),))
239 239 return lst
240 240
241 241 def populate_obj(self, populate_dict):
242 242 """populate model with data from given populate_dict"""
243 243
244 244 for k in self._get_keys():
245 245 if k in populate_dict:
246 246 setattr(self, k, populate_dict[k])
247 247
248 248 @classmethod
249 249 def query(cls):
250 250 return Session().query(cls)
251 251
252 252 @classmethod
253 253 def get(cls, id_):
254 254 if id_:
255 255 return cls.query().get(id_)
256 256
257 257 @classmethod
258 258 def get_or_404(cls, id_):
259 259 from pyramid.httpexceptions import HTTPNotFound
260 260
261 261 try:
262 262 id_ = int(id_)
263 263 except (TypeError, ValueError):
264 264 raise HTTPNotFound()
265 265
266 266 res = cls.query().get(id_)
267 267 if not res:
268 268 raise HTTPNotFound()
269 269 return res
270 270
271 271 @classmethod
272 272 def getAll(cls):
273 273 # deprecated and left for backward compatibility
274 274 return cls.get_all()
275 275
276 276 @classmethod
277 277 def get_all(cls):
278 278 return cls.query().all()
279 279
280 280 @classmethod
281 281 def delete(cls, id_):
282 282 obj = cls.query().get(id_)
283 283 Session().delete(obj)
284 284
285 285 @classmethod
286 286 def identity_cache(cls, session, attr_name, value):
287 287 exist_in_session = []
288 288 for (item_cls, pkey), instance in session.identity_map.items():
289 289 if cls == item_cls and getattr(instance, attr_name) == value:
290 290 exist_in_session.append(instance)
291 291 if exist_in_session:
292 292 if len(exist_in_session) == 1:
293 293 return exist_in_session[0]
294 294 log.exception(
295 295 'multiple objects with attr %s and '
296 296 'value %s found with same name: %r',
297 297 attr_name, value, exist_in_session)
298 298
299 299 def __repr__(self):
300 300 if hasattr(self, '__unicode__'):
301 301 # python repr needs to return str
302 302 try:
303 303 return safe_str(self.__unicode__())
304 304 except UnicodeDecodeError:
305 305 pass
306 306 return '<DB:%s>' % (self.__class__.__name__)
307 307
308 308
309 309 class RhodeCodeSetting(Base, BaseModel):
310 310 __tablename__ = 'rhodecode_settings'
311 311 __table_args__ = (
312 312 UniqueConstraint('app_settings_name'),
313 313 base_table_args
314 314 )
315 315
316 316 SETTINGS_TYPES = {
317 317 'str': safe_str,
318 318 'int': safe_int,
319 319 'unicode': safe_unicode,
320 320 'bool': str2bool,
321 321 'list': functools.partial(aslist, sep=',')
322 322 }
323 323 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
324 324 GLOBAL_CONF_KEY = 'app_settings'
325 325
326 326 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
327 327 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
328 328 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
329 329 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
330 330
331 331 def __init__(self, key='', val='', type='unicode'):
332 332 self.app_settings_name = key
333 333 self.app_settings_type = type
334 334 self.app_settings_value = val
335 335
336 336 @validates('_app_settings_value')
337 337 def validate_settings_value(self, key, val):
338 338 assert type(val) == unicode
339 339 return val
340 340
341 341 @hybrid_property
342 342 def app_settings_value(self):
343 343 v = self._app_settings_value
344 344 _type = self.app_settings_type
345 345 if _type:
346 346 _type = self.app_settings_type.split('.')[0]
347 347 # decode the encrypted value
348 348 if 'encrypted' in self.app_settings_type:
349 349 cipher = EncryptedTextValue()
350 350 v = safe_unicode(cipher.process_result_value(v, None))
351 351
352 352 converter = self.SETTINGS_TYPES.get(_type) or \
353 353 self.SETTINGS_TYPES['unicode']
354 354 return converter(v)
355 355
356 356 @app_settings_value.setter
357 357 def app_settings_value(self, val):
358 358 """
359 359 Setter that will always make sure we use unicode in app_settings_value
360 360
361 361 :param val:
362 362 """
363 363 val = safe_unicode(val)
364 364 # encode the encrypted value
365 365 if 'encrypted' in self.app_settings_type:
366 366 cipher = EncryptedTextValue()
367 367 val = safe_unicode(cipher.process_bind_param(val, None))
368 368 self._app_settings_value = val
369 369
370 370 @hybrid_property
371 371 def app_settings_type(self):
372 372 return self._app_settings_type
373 373
374 374 @app_settings_type.setter
375 375 def app_settings_type(self, val):
376 376 if val.split('.')[0] not in self.SETTINGS_TYPES:
377 377 raise Exception('type must be one of %s got %s'
378 378 % (self.SETTINGS_TYPES.keys(), val))
379 379 self._app_settings_type = val
380 380
381 381 def __unicode__(self):
382 382 return u"<%s('%s:%s[%s]')>" % (
383 383 self.__class__.__name__,
384 384 self.app_settings_name, self.app_settings_value,
385 385 self.app_settings_type
386 386 )
387 387
388 388
389 389 class RhodeCodeUi(Base, BaseModel):
390 390 __tablename__ = 'rhodecode_ui'
391 391 __table_args__ = (
392 392 UniqueConstraint('ui_key'),
393 393 base_table_args
394 394 )
395 395
396 396 HOOK_REPO_SIZE = 'changegroup.repo_size'
397 397 # HG
398 398 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
399 399 HOOK_PULL = 'outgoing.pull_logger'
400 400 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
401 401 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
402 402 HOOK_PUSH = 'changegroup.push_logger'
403 403 HOOK_PUSH_KEY = 'pushkey.key_push'
404 404
405 405 # TODO: johbo: Unify way how hooks are configured for git and hg,
406 406 # git part is currently hardcoded.
407 407
408 408 # SVN PATTERNS
409 409 SVN_BRANCH_ID = 'vcs_svn_branch'
410 410 SVN_TAG_ID = 'vcs_svn_tag'
411 411
412 412 ui_id = Column(
413 413 "ui_id", Integer(), nullable=False, unique=True, default=None,
414 414 primary_key=True)
415 415 ui_section = Column(
416 416 "ui_section", String(255), nullable=True, unique=None, default=None)
417 417 ui_key = Column(
418 418 "ui_key", String(255), nullable=True, unique=None, default=None)
419 419 ui_value = Column(
420 420 "ui_value", String(255), nullable=True, unique=None, default=None)
421 421 ui_active = Column(
422 422 "ui_active", Boolean(), nullable=True, unique=None, default=True)
423 423
424 424 def __repr__(self):
425 425 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
426 426 self.ui_key, self.ui_value)
427 427
428 428
429 429 class RepoRhodeCodeSetting(Base, BaseModel):
430 430 __tablename__ = 'repo_rhodecode_settings'
431 431 __table_args__ = (
432 432 UniqueConstraint(
433 433 'app_settings_name', 'repository_id',
434 434 name='uq_repo_rhodecode_setting_name_repo_id'),
435 435 base_table_args
436 436 )
437 437
438 438 repository_id = Column(
439 439 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
440 440 nullable=False)
441 441 app_settings_id = Column(
442 442 "app_settings_id", Integer(), nullable=False, unique=True,
443 443 default=None, primary_key=True)
444 444 app_settings_name = Column(
445 445 "app_settings_name", String(255), nullable=True, unique=None,
446 446 default=None)
447 447 _app_settings_value = Column(
448 448 "app_settings_value", String(4096), nullable=True, unique=None,
449 449 default=None)
450 450 _app_settings_type = Column(
451 451 "app_settings_type", String(255), nullable=True, unique=None,
452 452 default=None)
453 453
454 454 repository = relationship('Repository')
455 455
456 456 def __init__(self, repository_id, key='', val='', type='unicode'):
457 457 self.repository_id = repository_id
458 458 self.app_settings_name = key
459 459 self.app_settings_type = type
460 460 self.app_settings_value = val
461 461
462 462 @validates('_app_settings_value')
463 463 def validate_settings_value(self, key, val):
464 464 assert type(val) == unicode
465 465 return val
466 466
467 467 @hybrid_property
468 468 def app_settings_value(self):
469 469 v = self._app_settings_value
470 470 type_ = self.app_settings_type
471 471 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
472 472 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
473 473 return converter(v)
474 474
475 475 @app_settings_value.setter
476 476 def app_settings_value(self, val):
477 477 """
478 478 Setter that will always make sure we use unicode in app_settings_value
479 479
480 480 :param val:
481 481 """
482 482 self._app_settings_value = safe_unicode(val)
483 483
484 484 @hybrid_property
485 485 def app_settings_type(self):
486 486 return self._app_settings_type
487 487
488 488 @app_settings_type.setter
489 489 def app_settings_type(self, val):
490 490 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
491 491 if val not in SETTINGS_TYPES:
492 492 raise Exception('type must be one of %s got %s'
493 493 % (SETTINGS_TYPES.keys(), val))
494 494 self._app_settings_type = val
495 495
496 496 def __unicode__(self):
497 497 return u"<%s('%s:%s:%s[%s]')>" % (
498 498 self.__class__.__name__, self.repository.repo_name,
499 499 self.app_settings_name, self.app_settings_value,
500 500 self.app_settings_type
501 501 )
502 502
503 503
504 504 class RepoRhodeCodeUi(Base, BaseModel):
505 505 __tablename__ = 'repo_rhodecode_ui'
506 506 __table_args__ = (
507 507 UniqueConstraint(
508 508 'repository_id', 'ui_section', 'ui_key',
509 509 name='uq_repo_rhodecode_ui_repository_id_section_key'),
510 510 base_table_args
511 511 )
512 512
513 513 repository_id = Column(
514 514 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
515 515 nullable=False)
516 516 ui_id = Column(
517 517 "ui_id", Integer(), nullable=False, unique=True, default=None,
518 518 primary_key=True)
519 519 ui_section = Column(
520 520 "ui_section", String(255), nullable=True, unique=None, default=None)
521 521 ui_key = Column(
522 522 "ui_key", String(255), nullable=True, unique=None, default=None)
523 523 ui_value = Column(
524 524 "ui_value", String(255), nullable=True, unique=None, default=None)
525 525 ui_active = Column(
526 526 "ui_active", Boolean(), nullable=True, unique=None, default=True)
527 527
528 528 repository = relationship('Repository')
529 529
530 530 def __repr__(self):
531 531 return '<%s[%s:%s]%s=>%s]>' % (
532 532 self.__class__.__name__, self.repository.repo_name,
533 533 self.ui_section, self.ui_key, self.ui_value)
534 534
535 535
536 536 class User(Base, BaseModel):
537 537 __tablename__ = 'users'
538 538 __table_args__ = (
539 539 UniqueConstraint('username'), UniqueConstraint('email'),
540 540 Index('u_username_idx', 'username'),
541 541 Index('u_email_idx', 'email'),
542 542 base_table_args
543 543 )
544 544
545 545 DEFAULT_USER = 'default'
546 546 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
547 547 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
548 548
549 549 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
550 550 username = Column("username", String(255), nullable=True, unique=None, default=None)
551 551 password = Column("password", String(255), nullable=True, unique=None, default=None)
552 552 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
553 553 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
554 554 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
555 555 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
556 556 _email = Column("email", String(255), nullable=True, unique=None, default=None)
557 557 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
558 558 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
559 559
560 560 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
561 561 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
562 562 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
563 563 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
564 564 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
565 565 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
566 566
567 567 user_log = relationship('UserLog')
568 568 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
569 569
570 570 repositories = relationship('Repository')
571 571 repository_groups = relationship('RepoGroup')
572 572 user_groups = relationship('UserGroup')
573 573
574 574 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
575 575 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
576 576
577 577 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
578 578 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
579 579 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
580 580
581 581 group_member = relationship('UserGroupMember', cascade='all')
582 582
583 583 notifications = relationship('UserNotification', cascade='all')
584 584 # notifications assigned to this user
585 585 user_created_notifications = relationship('Notification', cascade='all')
586 586 # comments created by this user
587 587 user_comments = relationship('ChangesetComment', cascade='all')
588 588 # user profile extra info
589 589 user_emails = relationship('UserEmailMap', cascade='all')
590 590 user_ip_map = relationship('UserIpMap', cascade='all')
591 591 user_auth_tokens = relationship('UserApiKeys', cascade='all')
592 592 user_ssh_keys = relationship('UserSshKeys', cascade='all')
593 593
594 594 # gists
595 595 user_gists = relationship('Gist', cascade='all')
596 596 # user pull requests
597 597 user_pull_requests = relationship('PullRequest', cascade='all')
598 598 # external identities
599 599 extenal_identities = relationship(
600 600 'ExternalIdentity',
601 601 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
602 602 cascade='all')
603 603 # review rules
604 604 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
605 605
606 606 def __unicode__(self):
607 607 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
608 608 self.user_id, self.username)
609 609
610 610 @hybrid_property
611 611 def email(self):
612 612 return self._email
613 613
614 614 @email.setter
615 615 def email(self, val):
616 616 self._email = val.lower() if val else None
617 617
618 618 @hybrid_property
619 619 def first_name(self):
620 620 from rhodecode.lib import helpers as h
621 621 if self.name:
622 622 return h.escape(self.name)
623 623 return self.name
624 624
625 625 @hybrid_property
626 626 def last_name(self):
627 627 from rhodecode.lib import helpers as h
628 628 if self.lastname:
629 629 return h.escape(self.lastname)
630 630 return self.lastname
631 631
632 632 @hybrid_property
633 633 def api_key(self):
634 634 """
635 635 Fetch if exist an auth-token with role ALL connected to this user
636 636 """
637 637 user_auth_token = UserApiKeys.query()\
638 638 .filter(UserApiKeys.user_id == self.user_id)\
639 639 .filter(or_(UserApiKeys.expires == -1,
640 640 UserApiKeys.expires >= time.time()))\
641 641 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
642 642 if user_auth_token:
643 643 user_auth_token = user_auth_token.api_key
644 644
645 645 return user_auth_token
646 646
647 647 @api_key.setter
648 648 def api_key(self, val):
649 649 # don't allow to set API key this is deprecated for now
650 650 self._api_key = None
651 651
652 652 @property
653 653 def reviewer_pull_requests(self):
654 654 return PullRequestReviewers.query() \
655 655 .options(joinedload(PullRequestReviewers.pull_request)) \
656 656 .filter(PullRequestReviewers.user_id == self.user_id) \
657 657 .all()
658 658
659 659 @property
660 660 def firstname(self):
661 661 # alias for future
662 662 return self.name
663 663
664 664 @property
665 665 def emails(self):
666 666 other = UserEmailMap.query()\
667 667 .filter(UserEmailMap.user == self) \
668 668 .order_by(UserEmailMap.email_id.asc()) \
669 669 .all()
670 670 return [self.email] + [x.email for x in other]
671 671
672 672 @property
673 673 def auth_tokens(self):
674 674 auth_tokens = self.get_auth_tokens()
675 675 return [x.api_key for x in auth_tokens]
676 676
677 677 def get_auth_tokens(self):
678 678 return UserApiKeys.query()\
679 679 .filter(UserApiKeys.user == self)\
680 680 .order_by(UserApiKeys.user_api_key_id.asc())\
681 681 .all()
682 682
683 683 @LazyProperty
684 684 def feed_token(self):
685 685 return self.get_feed_token()
686 686
687 687 def get_feed_token(self, cache=True):
688 688 feed_tokens = UserApiKeys.query()\
689 689 .filter(UserApiKeys.user == self)\
690 690 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
691 691 if cache:
692 692 feed_tokens = feed_tokens.options(
693 693 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
694 694
695 695 feed_tokens = feed_tokens.all()
696 696 if feed_tokens:
697 697 return feed_tokens[0].api_key
698 698 return 'NO_FEED_TOKEN_AVAILABLE'
699 699
700 700 @classmethod
701 701 def get(cls, user_id, cache=False):
702 702 if not user_id:
703 703 return
704 704
705 705 user = cls.query()
706 706 if cache:
707 707 user = user.options(
708 708 FromCache("sql_cache_short", "get_users_%s" % user_id))
709 709 return user.get(user_id)
710 710
711 711 @classmethod
712 712 def extra_valid_auth_tokens(cls, user, role=None):
713 713 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
714 714 .filter(or_(UserApiKeys.expires == -1,
715 715 UserApiKeys.expires >= time.time()))
716 716 if role:
717 717 tokens = tokens.filter(or_(UserApiKeys.role == role,
718 718 UserApiKeys.role == UserApiKeys.ROLE_ALL))
719 719 return tokens.all()
720 720
721 721 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
722 722 from rhodecode.lib import auth
723 723
724 724 log.debug('Trying to authenticate user: %s via auth-token, '
725 725 'and roles: %s', self, roles)
726 726
727 727 if not auth_token:
728 728 return False
729 729
730 730 crypto_backend = auth.crypto_backend()
731 731
732 732 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
733 733 tokens_q = UserApiKeys.query()\
734 734 .filter(UserApiKeys.user_id == self.user_id)\
735 735 .filter(or_(UserApiKeys.expires == -1,
736 736 UserApiKeys.expires >= time.time()))
737 737
738 738 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
739 739
740 740 plain_tokens = []
741 741 hash_tokens = []
742 742
743 743 for token in tokens_q.all():
744 744 # verify scope first
745 745 if token.repo_id:
746 746 # token has a scope, we need to verify it
747 747 if scope_repo_id != token.repo_id:
748 748 log.debug(
749 749 'Scope mismatch: token has a set repo scope: %s, '
750 750 'and calling scope is:%s, skipping further checks',
751 751 token.repo, scope_repo_id)
752 752 # token has a scope, and it doesn't match, skip token
753 753 continue
754 754
755 755 if token.api_key.startswith(crypto_backend.ENC_PREF):
756 756 hash_tokens.append(token.api_key)
757 757 else:
758 758 plain_tokens.append(token.api_key)
759 759
760 760 is_plain_match = auth_token in plain_tokens
761 761 if is_plain_match:
762 762 return True
763 763
764 764 for hashed in hash_tokens:
765 765 # TODO(marcink): this is expensive to calculate, but most secure
766 766 match = crypto_backend.hash_check(auth_token, hashed)
767 767 if match:
768 768 return True
769 769
770 770 return False
771 771
772 772 @property
773 773 def ip_addresses(self):
774 774 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
775 775 return [x.ip_addr for x in ret]
776 776
777 777 @property
778 778 def username_and_name(self):
779 779 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
780 780
781 781 @property
782 782 def username_or_name_or_email(self):
783 783 full_name = self.full_name if self.full_name is not ' ' else None
784 784 return self.username or full_name or self.email
785 785
786 786 @property
787 787 def full_name(self):
788 788 return '%s %s' % (self.first_name, self.last_name)
789 789
790 790 @property
791 791 def full_name_or_username(self):
792 792 return ('%s %s' % (self.first_name, self.last_name)
793 793 if (self.first_name and self.last_name) else self.username)
794 794
795 795 @property
796 796 def full_contact(self):
797 797 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
798 798
799 799 @property
800 800 def short_contact(self):
801 801 return '%s %s' % (self.first_name, self.last_name)
802 802
803 803 @property
804 804 def is_admin(self):
805 805 return self.admin
806 806
807 807 def AuthUser(self, **kwargs):
808 808 """
809 809 Returns instance of AuthUser for this user
810 810 """
811 811 from rhodecode.lib.auth import AuthUser
812 812 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
813 813
814 814 @hybrid_property
815 815 def user_data(self):
816 816 if not self._user_data:
817 817 return {}
818 818
819 819 try:
820 820 return json.loads(self._user_data)
821 821 except TypeError:
822 822 return {}
823 823
824 824 @user_data.setter
825 825 def user_data(self, val):
826 826 if not isinstance(val, dict):
827 827 raise Exception('user_data must be dict, got %s' % type(val))
828 828 try:
829 829 self._user_data = json.dumps(val)
830 830 except Exception:
831 831 log.error(traceback.format_exc())
832 832
833 833 @classmethod
834 834 def get_by_username(cls, username, case_insensitive=False,
835 835 cache=False, identity_cache=False):
836 836 session = Session()
837 837
838 838 if case_insensitive:
839 839 q = cls.query().filter(
840 840 func.lower(cls.username) == func.lower(username))
841 841 else:
842 842 q = cls.query().filter(cls.username == username)
843 843
844 844 if cache:
845 845 if identity_cache:
846 846 val = cls.identity_cache(session, 'username', username)
847 847 if val:
848 848 return val
849 849 else:
850 850 cache_key = "get_user_by_name_%s" % _hash_key(username)
851 851 q = q.options(
852 852 FromCache("sql_cache_short", cache_key))
853 853
854 854 return q.scalar()
855 855
856 856 @classmethod
857 857 def get_by_auth_token(cls, auth_token, cache=False):
858 858 q = UserApiKeys.query()\
859 859 .filter(UserApiKeys.api_key == auth_token)\
860 860 .filter(or_(UserApiKeys.expires == -1,
861 861 UserApiKeys.expires >= time.time()))
862 862 if cache:
863 863 q = q.options(
864 864 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
865 865
866 866 match = q.first()
867 867 if match:
868 868 return match.user
869 869
870 870 @classmethod
871 871 def get_by_email(cls, email, case_insensitive=False, cache=False):
872 872
873 873 if case_insensitive:
874 874 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
875 875
876 876 else:
877 877 q = cls.query().filter(cls.email == email)
878 878
879 879 email_key = _hash_key(email)
880 880 if cache:
881 881 q = q.options(
882 882 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
883 883
884 884 ret = q.scalar()
885 885 if ret is None:
886 886 q = UserEmailMap.query()
887 887 # try fetching in alternate email map
888 888 if case_insensitive:
889 889 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
890 890 else:
891 891 q = q.filter(UserEmailMap.email == email)
892 892 q = q.options(joinedload(UserEmailMap.user))
893 893 if cache:
894 894 q = q.options(
895 895 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
896 896 ret = getattr(q.scalar(), 'user', None)
897 897
898 898 return ret
899 899
900 900 @classmethod
901 901 def get_from_cs_author(cls, author):
902 902 """
903 903 Tries to get User objects out of commit author string
904 904
905 905 :param author:
906 906 """
907 907 from rhodecode.lib.helpers import email, author_name
908 908 # Valid email in the attribute passed, see if they're in the system
909 909 _email = email(author)
910 910 if _email:
911 911 user = cls.get_by_email(_email, case_insensitive=True)
912 912 if user:
913 913 return user
914 914 # Maybe we can match by username?
915 915 _author = author_name(author)
916 916 user = cls.get_by_username(_author, case_insensitive=True)
917 917 if user:
918 918 return user
919 919
920 920 def update_userdata(self, **kwargs):
921 921 usr = self
922 922 old = usr.user_data
923 923 old.update(**kwargs)
924 924 usr.user_data = old
925 925 Session().add(usr)
926 926 log.debug('updated userdata with ', kwargs)
927 927
928 928 def update_lastlogin(self):
929 929 """Update user lastlogin"""
930 930 self.last_login = datetime.datetime.now()
931 931 Session().add(self)
932 932 log.debug('updated user %s lastlogin', self.username)
933 933
934 def update_lastactivity(self):
935 """Update user lastactivity"""
936 self.last_activity = datetime.datetime.now()
937 Session().add(self)
938 log.debug('updated user `%s` last activity', self.username)
939
940 934 def update_password(self, new_password):
941 935 from rhodecode.lib.auth import get_crypt_password
942 936
943 937 self.password = get_crypt_password(new_password)
944 938 Session().add(self)
945 939
946 940 @classmethod
947 941 def get_first_super_admin(cls):
948 942 user = User.query().filter(User.admin == true()).first()
949 943 if user is None:
950 944 raise Exception('FATAL: Missing administrative account!')
951 945 return user
952 946
953 947 @classmethod
954 948 def get_all_super_admins(cls):
955 949 """
956 950 Returns all admin accounts sorted by username
957 951 """
958 952 return User.query().filter(User.admin == true())\
959 953 .order_by(User.username.asc()).all()
960 954
961 955 @classmethod
962 956 def get_default_user(cls, cache=False, refresh=False):
963 957 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
964 958 if user is None:
965 959 raise Exception('FATAL: Missing default account!')
966 960 if refresh:
967 961 # The default user might be based on outdated state which
968 962 # has been loaded from the cache.
969 963 # A call to refresh() ensures that the
970 964 # latest state from the database is used.
971 965 Session().refresh(user)
972 966 return user
973 967
974 968 def _get_default_perms(self, user, suffix=''):
975 969 from rhodecode.model.permission import PermissionModel
976 970 return PermissionModel().get_default_perms(user.user_perms, suffix)
977 971
978 972 def get_default_perms(self, suffix=''):
979 973 return self._get_default_perms(self, suffix)
980 974
981 975 def get_api_data(self, include_secrets=False, details='full'):
982 976 """
983 977 Common function for generating user related data for API
984 978
985 979 :param include_secrets: By default secrets in the API data will be replaced
986 980 by a placeholder value to prevent exposing this data by accident. In case
987 981 this data shall be exposed, set this flag to ``True``.
988 982
989 983 :param details: details can be 'basic|full' basic gives only a subset of
990 984 the available user information that includes user_id, name and emails.
991 985 """
992 986 user = self
993 987 user_data = self.user_data
994 988 data = {
995 989 'user_id': user.user_id,
996 990 'username': user.username,
997 991 'firstname': user.name,
998 992 'lastname': user.lastname,
999 993 'email': user.email,
1000 994 'emails': user.emails,
1001 995 }
1002 996 if details == 'basic':
1003 997 return data
1004 998
1005 999 auth_token_length = 40
1006 1000 auth_token_replacement = '*' * auth_token_length
1007 1001
1008 1002 extras = {
1009 1003 'auth_tokens': [auth_token_replacement],
1010 1004 'active': user.active,
1011 1005 'admin': user.admin,
1012 1006 'extern_type': user.extern_type,
1013 1007 'extern_name': user.extern_name,
1014 1008 'last_login': user.last_login,
1015 1009 'last_activity': user.last_activity,
1016 1010 'ip_addresses': user.ip_addresses,
1017 1011 'language': user_data.get('language')
1018 1012 }
1019 1013 data.update(extras)
1020 1014
1021 1015 if include_secrets:
1022 1016 data['auth_tokens'] = user.auth_tokens
1023 1017 return data
1024 1018
1025 1019 def __json__(self):
1026 1020 data = {
1027 1021 'full_name': self.full_name,
1028 1022 'full_name_or_username': self.full_name_or_username,
1029 1023 'short_contact': self.short_contact,
1030 1024 'full_contact': self.full_contact,
1031 1025 }
1032 1026 data.update(self.get_api_data())
1033 1027 return data
1034 1028
1035 1029
1036 1030 class UserApiKeys(Base, BaseModel):
1037 1031 __tablename__ = 'user_api_keys'
1038 1032 __table_args__ = (
1039 1033 Index('uak_api_key_idx', 'api_key', unique=True),
1040 1034 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1041 1035 base_table_args
1042 1036 )
1043 1037 __mapper_args__ = {}
1044 1038
1045 1039 # ApiKey role
1046 1040 ROLE_ALL = 'token_role_all'
1047 1041 ROLE_HTTP = 'token_role_http'
1048 1042 ROLE_VCS = 'token_role_vcs'
1049 1043 ROLE_API = 'token_role_api'
1050 1044 ROLE_FEED = 'token_role_feed'
1051 1045 ROLE_PASSWORD_RESET = 'token_password_reset'
1052 1046
1053 1047 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
1054 1048
1055 1049 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1056 1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1057 1051 api_key = Column("api_key", String(255), nullable=False, unique=True)
1058 1052 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1059 1053 expires = Column('expires', Float(53), nullable=False)
1060 1054 role = Column('role', String(255), nullable=True)
1061 1055 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1062 1056
1063 1057 # scope columns
1064 1058 repo_id = Column(
1065 1059 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1066 1060 nullable=True, unique=None, default=None)
1067 1061 repo = relationship('Repository', lazy='joined')
1068 1062
1069 1063 repo_group_id = Column(
1070 1064 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1071 1065 nullable=True, unique=None, default=None)
1072 1066 repo_group = relationship('RepoGroup', lazy='joined')
1073 1067
1074 1068 user = relationship('User', lazy='joined')
1075 1069
1076 1070 def __unicode__(self):
1077 1071 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1078 1072
1079 1073 def __json__(self):
1080 1074 data = {
1081 1075 'auth_token': self.api_key,
1082 1076 'role': self.role,
1083 1077 'scope': self.scope_humanized,
1084 1078 'expired': self.expired
1085 1079 }
1086 1080 return data
1087 1081
1088 1082 def get_api_data(self, include_secrets=False):
1089 1083 data = self.__json__()
1090 1084 if include_secrets:
1091 1085 return data
1092 1086 else:
1093 1087 data['auth_token'] = self.token_obfuscated
1094 1088 return data
1095 1089
1096 1090 @hybrid_property
1097 1091 def description_safe(self):
1098 1092 from rhodecode.lib import helpers as h
1099 1093 return h.escape(self.description)
1100 1094
1101 1095 @property
1102 1096 def expired(self):
1103 1097 if self.expires == -1:
1104 1098 return False
1105 1099 return time.time() > self.expires
1106 1100
1107 1101 @classmethod
1108 1102 def _get_role_name(cls, role):
1109 1103 return {
1110 1104 cls.ROLE_ALL: _('all'),
1111 1105 cls.ROLE_HTTP: _('http/web interface'),
1112 1106 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1113 1107 cls.ROLE_API: _('api calls'),
1114 1108 cls.ROLE_FEED: _('feed access'),
1115 1109 }.get(role, role)
1116 1110
1117 1111 @property
1118 1112 def role_humanized(self):
1119 1113 return self._get_role_name(self.role)
1120 1114
1121 1115 def _get_scope(self):
1122 1116 if self.repo:
1123 1117 return repr(self.repo)
1124 1118 if self.repo_group:
1125 1119 return repr(self.repo_group) + ' (recursive)'
1126 1120 return 'global'
1127 1121
1128 1122 @property
1129 1123 def scope_humanized(self):
1130 1124 return self._get_scope()
1131 1125
1132 1126 @property
1133 1127 def token_obfuscated(self):
1134 1128 if self.api_key:
1135 1129 return self.api_key[:4] + "****"
1136 1130
1137 1131
1138 1132 class UserEmailMap(Base, BaseModel):
1139 1133 __tablename__ = 'user_email_map'
1140 1134 __table_args__ = (
1141 1135 Index('uem_email_idx', 'email'),
1142 1136 UniqueConstraint('email'),
1143 1137 base_table_args
1144 1138 )
1145 1139 __mapper_args__ = {}
1146 1140
1147 1141 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1148 1142 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1149 1143 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1150 1144 user = relationship('User', lazy='joined')
1151 1145
1152 1146 @validates('_email')
1153 1147 def validate_email(self, key, email):
1154 1148 # check if this email is not main one
1155 1149 main_email = Session().query(User).filter(User.email == email).scalar()
1156 1150 if main_email is not None:
1157 1151 raise AttributeError('email %s is present is user table' % email)
1158 1152 return email
1159 1153
1160 1154 @hybrid_property
1161 1155 def email(self):
1162 1156 return self._email
1163 1157
1164 1158 @email.setter
1165 1159 def email(self, val):
1166 1160 self._email = val.lower() if val else None
1167 1161
1168 1162
1169 1163 class UserIpMap(Base, BaseModel):
1170 1164 __tablename__ = 'user_ip_map'
1171 1165 __table_args__ = (
1172 1166 UniqueConstraint('user_id', 'ip_addr'),
1173 1167 base_table_args
1174 1168 )
1175 1169 __mapper_args__ = {}
1176 1170
1177 1171 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1178 1172 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1179 1173 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1180 1174 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1181 1175 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1182 1176 user = relationship('User', lazy='joined')
1183 1177
1184 1178 @hybrid_property
1185 1179 def description_safe(self):
1186 1180 from rhodecode.lib import helpers as h
1187 1181 return h.escape(self.description)
1188 1182
1189 1183 @classmethod
1190 1184 def _get_ip_range(cls, ip_addr):
1191 1185 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1192 1186 return [str(net.network_address), str(net.broadcast_address)]
1193 1187
1194 1188 def __json__(self):
1195 1189 return {
1196 1190 'ip_addr': self.ip_addr,
1197 1191 'ip_range': self._get_ip_range(self.ip_addr),
1198 1192 }
1199 1193
1200 1194 def __unicode__(self):
1201 1195 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1202 1196 self.user_id, self.ip_addr)
1203 1197
1204 1198
1205 1199 class UserSshKeys(Base, BaseModel):
1206 1200 __tablename__ = 'user_ssh_keys'
1207 1201 __table_args__ = (
1208 1202 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1209 1203
1210 1204 UniqueConstraint('ssh_key_fingerprint'),
1211 1205
1212 1206 base_table_args
1213 1207 )
1214 1208 __mapper_args__ = {}
1215 1209
1216 1210 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1217 1211 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1218 1212 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1219 1213
1220 1214 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1221 1215
1222 1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1223 1217 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1224 1218 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1225 1219
1226 1220 user = relationship('User', lazy='joined')
1227 1221
1228 1222 def __json__(self):
1229 1223 data = {
1230 1224 'ssh_fingerprint': self.ssh_key_fingerprint,
1231 1225 'description': self.description,
1232 1226 'created_on': self.created_on
1233 1227 }
1234 1228 return data
1235 1229
1236 1230 def get_api_data(self):
1237 1231 data = self.__json__()
1238 1232 return data
1239 1233
1240 1234
1241 1235 class UserLog(Base, BaseModel):
1242 1236 __tablename__ = 'user_logs'
1243 1237 __table_args__ = (
1244 1238 base_table_args,
1245 1239 )
1246 1240
1247 1241 VERSION_1 = 'v1'
1248 1242 VERSION_2 = 'v2'
1249 1243 VERSIONS = [VERSION_1, VERSION_2]
1250 1244
1251 1245 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1252 1246 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1253 1247 username = Column("username", String(255), nullable=True, unique=None, default=None)
1254 1248 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1255 1249 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1256 1250 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1257 1251 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1258 1252 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1259 1253
1260 1254 version = Column("version", String(255), nullable=True, default=VERSION_1)
1261 1255 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1262 1256 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1263 1257
1264 1258 def __unicode__(self):
1265 1259 return u"<%s('id:%s:%s')>" % (
1266 1260 self.__class__.__name__, self.repository_name, self.action)
1267 1261
1268 1262 def __json__(self):
1269 1263 return {
1270 1264 'user_id': self.user_id,
1271 1265 'username': self.username,
1272 1266 'repository_id': self.repository_id,
1273 1267 'repository_name': self.repository_name,
1274 1268 'user_ip': self.user_ip,
1275 1269 'action_date': self.action_date,
1276 1270 'action': self.action,
1277 1271 }
1278 1272
1279 1273 @hybrid_property
1280 1274 def entry_id(self):
1281 1275 return self.user_log_id
1282 1276
1283 1277 @property
1284 1278 def action_as_day(self):
1285 1279 return datetime.date(*self.action_date.timetuple()[:3])
1286 1280
1287 1281 user = relationship('User')
1288 1282 repository = relationship('Repository', cascade='')
1289 1283
1290 1284
1291 1285 class UserGroup(Base, BaseModel):
1292 1286 __tablename__ = 'users_groups'
1293 1287 __table_args__ = (
1294 1288 base_table_args,
1295 1289 )
1296 1290
1297 1291 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1298 1292 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1299 1293 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1300 1294 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1301 1295 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1302 1296 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1303 1297 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1304 1298 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1305 1299
1306 1300 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1307 1301 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1308 1302 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1309 1303 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1310 1304 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1311 1305 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1312 1306
1313 1307 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1314 1308 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1315 1309
1316 1310 @classmethod
1317 1311 def _load_group_data(cls, column):
1318 1312 if not column:
1319 1313 return {}
1320 1314
1321 1315 try:
1322 1316 return json.loads(column) or {}
1323 1317 except TypeError:
1324 1318 return {}
1325 1319
1326 1320 @hybrid_property
1327 1321 def description_safe(self):
1328 1322 from rhodecode.lib import helpers as h
1329 1323 return h.escape(self.user_group_description)
1330 1324
1331 1325 @hybrid_property
1332 1326 def group_data(self):
1333 1327 return self._load_group_data(self._group_data)
1334 1328
1335 1329 @group_data.expression
1336 1330 def group_data(self, **kwargs):
1337 1331 return self._group_data
1338 1332
1339 1333 @group_data.setter
1340 1334 def group_data(self, val):
1341 1335 try:
1342 1336 self._group_data = json.dumps(val)
1343 1337 except Exception:
1344 1338 log.error(traceback.format_exc())
1345 1339
1346 1340 @classmethod
1347 1341 def _load_sync(cls, group_data):
1348 1342 if group_data:
1349 1343 return group_data.get('extern_type')
1350 1344
1351 1345 @property
1352 1346 def sync(self):
1353 1347 return self._load_sync(self.group_data)
1354 1348
1355 1349 def __unicode__(self):
1356 1350 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1357 1351 self.users_group_id,
1358 1352 self.users_group_name)
1359 1353
1360 1354 @classmethod
1361 1355 def get_by_group_name(cls, group_name, cache=False,
1362 1356 case_insensitive=False):
1363 1357 if case_insensitive:
1364 1358 q = cls.query().filter(func.lower(cls.users_group_name) ==
1365 1359 func.lower(group_name))
1366 1360
1367 1361 else:
1368 1362 q = cls.query().filter(cls.users_group_name == group_name)
1369 1363 if cache:
1370 1364 q = q.options(
1371 1365 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1372 1366 return q.scalar()
1373 1367
1374 1368 @classmethod
1375 1369 def get(cls, user_group_id, cache=False):
1376 1370 if not user_group_id:
1377 1371 return
1378 1372
1379 1373 user_group = cls.query()
1380 1374 if cache:
1381 1375 user_group = user_group.options(
1382 1376 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1383 1377 return user_group.get(user_group_id)
1384 1378
1385 1379 def permissions(self, with_admins=True, with_owner=True):
1386 1380 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1387 1381 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1388 1382 joinedload(UserUserGroupToPerm.user),
1389 1383 joinedload(UserUserGroupToPerm.permission),)
1390 1384
1391 1385 # get owners and admins and permissions. We do a trick of re-writing
1392 1386 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1393 1387 # has a global reference and changing one object propagates to all
1394 1388 # others. This means if admin is also an owner admin_row that change
1395 1389 # would propagate to both objects
1396 1390 perm_rows = []
1397 1391 for _usr in q.all():
1398 1392 usr = AttributeDict(_usr.user.get_dict())
1399 1393 usr.permission = _usr.permission.permission_name
1400 1394 perm_rows.append(usr)
1401 1395
1402 1396 # filter the perm rows by 'default' first and then sort them by
1403 1397 # admin,write,read,none permissions sorted again alphabetically in
1404 1398 # each group
1405 1399 perm_rows = sorted(perm_rows, key=display_user_sort)
1406 1400
1407 1401 _admin_perm = 'usergroup.admin'
1408 1402 owner_row = []
1409 1403 if with_owner:
1410 1404 usr = AttributeDict(self.user.get_dict())
1411 1405 usr.owner_row = True
1412 1406 usr.permission = _admin_perm
1413 1407 owner_row.append(usr)
1414 1408
1415 1409 super_admin_rows = []
1416 1410 if with_admins:
1417 1411 for usr in User.get_all_super_admins():
1418 1412 # if this admin is also owner, don't double the record
1419 1413 if usr.user_id == owner_row[0].user_id:
1420 1414 owner_row[0].admin_row = True
1421 1415 else:
1422 1416 usr = AttributeDict(usr.get_dict())
1423 1417 usr.admin_row = True
1424 1418 usr.permission = _admin_perm
1425 1419 super_admin_rows.append(usr)
1426 1420
1427 1421 return super_admin_rows + owner_row + perm_rows
1428 1422
1429 1423 def permission_user_groups(self):
1430 1424 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1431 1425 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1432 1426 joinedload(UserGroupUserGroupToPerm.target_user_group),
1433 1427 joinedload(UserGroupUserGroupToPerm.permission),)
1434 1428
1435 1429 perm_rows = []
1436 1430 for _user_group in q.all():
1437 1431 usr = AttributeDict(_user_group.user_group.get_dict())
1438 1432 usr.permission = _user_group.permission.permission_name
1439 1433 perm_rows.append(usr)
1440 1434
1441 1435 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1442 1436 return perm_rows
1443 1437
1444 1438 def _get_default_perms(self, user_group, suffix=''):
1445 1439 from rhodecode.model.permission import PermissionModel
1446 1440 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1447 1441
1448 1442 def get_default_perms(self, suffix=''):
1449 1443 return self._get_default_perms(self, suffix)
1450 1444
1451 1445 def get_api_data(self, with_group_members=True, include_secrets=False):
1452 1446 """
1453 1447 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1454 1448 basically forwarded.
1455 1449
1456 1450 """
1457 1451 user_group = self
1458 1452 data = {
1459 1453 'users_group_id': user_group.users_group_id,
1460 1454 'group_name': user_group.users_group_name,
1461 1455 'group_description': user_group.user_group_description,
1462 1456 'active': user_group.users_group_active,
1463 1457 'owner': user_group.user.username,
1464 1458 'sync': user_group.sync,
1465 1459 'owner_email': user_group.user.email,
1466 1460 }
1467 1461
1468 1462 if with_group_members:
1469 1463 users = []
1470 1464 for user in user_group.members:
1471 1465 user = user.user
1472 1466 users.append(user.get_api_data(include_secrets=include_secrets))
1473 1467 data['users'] = users
1474 1468
1475 1469 return data
1476 1470
1477 1471
1478 1472 class UserGroupMember(Base, BaseModel):
1479 1473 __tablename__ = 'users_groups_members'
1480 1474 __table_args__ = (
1481 1475 base_table_args,
1482 1476 )
1483 1477
1484 1478 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1485 1479 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1486 1480 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1487 1481
1488 1482 user = relationship('User', lazy='joined')
1489 1483 users_group = relationship('UserGroup')
1490 1484
1491 1485 def __init__(self, gr_id='', u_id=''):
1492 1486 self.users_group_id = gr_id
1493 1487 self.user_id = u_id
1494 1488
1495 1489
1496 1490 class RepositoryField(Base, BaseModel):
1497 1491 __tablename__ = 'repositories_fields'
1498 1492 __table_args__ = (
1499 1493 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1500 1494 base_table_args,
1501 1495 )
1502 1496
1503 1497 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1504 1498
1505 1499 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1506 1500 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1507 1501 field_key = Column("field_key", String(250))
1508 1502 field_label = Column("field_label", String(1024), nullable=False)
1509 1503 field_value = Column("field_value", String(10000), nullable=False)
1510 1504 field_desc = Column("field_desc", String(1024), nullable=False)
1511 1505 field_type = Column("field_type", String(255), nullable=False, unique=None)
1512 1506 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1513 1507
1514 1508 repository = relationship('Repository')
1515 1509
1516 1510 @property
1517 1511 def field_key_prefixed(self):
1518 1512 return 'ex_%s' % self.field_key
1519 1513
1520 1514 @classmethod
1521 1515 def un_prefix_key(cls, key):
1522 1516 if key.startswith(cls.PREFIX):
1523 1517 return key[len(cls.PREFIX):]
1524 1518 return key
1525 1519
1526 1520 @classmethod
1527 1521 def get_by_key_name(cls, key, repo):
1528 1522 row = cls.query()\
1529 1523 .filter(cls.repository == repo)\
1530 1524 .filter(cls.field_key == key).scalar()
1531 1525 return row
1532 1526
1533 1527
1534 1528 class Repository(Base, BaseModel):
1535 1529 __tablename__ = 'repositories'
1536 1530 __table_args__ = (
1537 1531 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1538 1532 base_table_args,
1539 1533 )
1540 1534 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1541 1535 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1542 1536 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1543 1537
1544 1538 STATE_CREATED = 'repo_state_created'
1545 1539 STATE_PENDING = 'repo_state_pending'
1546 1540 STATE_ERROR = 'repo_state_error'
1547 1541
1548 1542 LOCK_AUTOMATIC = 'lock_auto'
1549 1543 LOCK_API = 'lock_api'
1550 1544 LOCK_WEB = 'lock_web'
1551 1545 LOCK_PULL = 'lock_pull'
1552 1546
1553 1547 NAME_SEP = URL_SEP
1554 1548
1555 1549 repo_id = Column(
1556 1550 "repo_id", Integer(), nullable=False, unique=True, default=None,
1557 1551 primary_key=True)
1558 1552 _repo_name = Column(
1559 1553 "repo_name", Text(), nullable=False, default=None)
1560 1554 _repo_name_hash = Column(
1561 1555 "repo_name_hash", String(255), nullable=False, unique=True)
1562 1556 repo_state = Column("repo_state", String(255), nullable=True)
1563 1557
1564 1558 clone_uri = Column(
1565 1559 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1566 1560 default=None)
1567 1561 push_uri = Column(
1568 1562 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1569 1563 default=None)
1570 1564 repo_type = Column(
1571 1565 "repo_type", String(255), nullable=False, unique=False, default=None)
1572 1566 user_id = Column(
1573 1567 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1574 1568 unique=False, default=None)
1575 1569 private = Column(
1576 1570 "private", Boolean(), nullable=True, unique=None, default=None)
1577 1571 enable_statistics = Column(
1578 1572 "statistics", Boolean(), nullable=True, unique=None, default=True)
1579 1573 enable_downloads = Column(
1580 1574 "downloads", Boolean(), nullable=True, unique=None, default=True)
1581 1575 description = Column(
1582 1576 "description", String(10000), nullable=True, unique=None, default=None)
1583 1577 created_on = Column(
1584 1578 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1585 1579 default=datetime.datetime.now)
1586 1580 updated_on = Column(
1587 1581 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1588 1582 default=datetime.datetime.now)
1589 1583 _landing_revision = Column(
1590 1584 "landing_revision", String(255), nullable=False, unique=False,
1591 1585 default=None)
1592 1586 enable_locking = Column(
1593 1587 "enable_locking", Boolean(), nullable=False, unique=None,
1594 1588 default=False)
1595 1589 _locked = Column(
1596 1590 "locked", String(255), nullable=True, unique=False, default=None)
1597 1591 _changeset_cache = Column(
1598 1592 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1599 1593
1600 1594 fork_id = Column(
1601 1595 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1602 1596 nullable=True, unique=False, default=None)
1603 1597 group_id = Column(
1604 1598 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1605 1599 unique=False, default=None)
1606 1600
1607 1601 user = relationship('User', lazy='joined')
1608 1602 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1609 1603 group = relationship('RepoGroup', lazy='joined')
1610 1604 repo_to_perm = relationship(
1611 1605 'UserRepoToPerm', cascade='all',
1612 1606 order_by='UserRepoToPerm.repo_to_perm_id')
1613 1607 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1614 1608 stats = relationship('Statistics', cascade='all', uselist=False)
1615 1609
1616 1610 followers = relationship(
1617 1611 'UserFollowing',
1618 1612 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1619 1613 cascade='all')
1620 1614 extra_fields = relationship(
1621 1615 'RepositoryField', cascade="all, delete, delete-orphan")
1622 1616 logs = relationship('UserLog')
1623 1617 comments = relationship(
1624 1618 'ChangesetComment', cascade="all, delete, delete-orphan")
1625 1619 pull_requests_source = relationship(
1626 1620 'PullRequest',
1627 1621 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1628 1622 cascade="all, delete, delete-orphan")
1629 1623 pull_requests_target = relationship(
1630 1624 'PullRequest',
1631 1625 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1632 1626 cascade="all, delete, delete-orphan")
1633 1627 ui = relationship('RepoRhodeCodeUi', cascade="all")
1634 1628 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1635 1629 integrations = relationship('Integration',
1636 1630 cascade="all, delete, delete-orphan")
1637 1631
1638 1632 scoped_tokens = relationship('UserApiKeys', cascade="all")
1639 1633
1640 1634 def __unicode__(self):
1641 1635 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1642 1636 safe_unicode(self.repo_name))
1643 1637
1644 1638 @hybrid_property
1645 1639 def description_safe(self):
1646 1640 from rhodecode.lib import helpers as h
1647 1641 return h.escape(self.description)
1648 1642
1649 1643 @hybrid_property
1650 1644 def landing_rev(self):
1651 1645 # always should return [rev_type, rev]
1652 1646 if self._landing_revision:
1653 1647 _rev_info = self._landing_revision.split(':')
1654 1648 if len(_rev_info) < 2:
1655 1649 _rev_info.insert(0, 'rev')
1656 1650 return [_rev_info[0], _rev_info[1]]
1657 1651 return [None, None]
1658 1652
1659 1653 @landing_rev.setter
1660 1654 def landing_rev(self, val):
1661 1655 if ':' not in val:
1662 1656 raise ValueError('value must be delimited with `:` and consist '
1663 1657 'of <rev_type>:<rev>, got %s instead' % val)
1664 1658 self._landing_revision = val
1665 1659
1666 1660 @hybrid_property
1667 1661 def locked(self):
1668 1662 if self._locked:
1669 1663 user_id, timelocked, reason = self._locked.split(':')
1670 1664 lock_values = int(user_id), timelocked, reason
1671 1665 else:
1672 1666 lock_values = [None, None, None]
1673 1667 return lock_values
1674 1668
1675 1669 @locked.setter
1676 1670 def locked(self, val):
1677 1671 if val and isinstance(val, (list, tuple)):
1678 1672 self._locked = ':'.join(map(str, val))
1679 1673 else:
1680 1674 self._locked = None
1681 1675
1682 1676 @hybrid_property
1683 1677 def changeset_cache(self):
1684 1678 from rhodecode.lib.vcs.backends.base import EmptyCommit
1685 1679 dummy = EmptyCommit().__json__()
1686 1680 if not self._changeset_cache:
1687 1681 return dummy
1688 1682 try:
1689 1683 return json.loads(self._changeset_cache)
1690 1684 except TypeError:
1691 1685 return dummy
1692 1686 except Exception:
1693 1687 log.error(traceback.format_exc())
1694 1688 return dummy
1695 1689
1696 1690 @changeset_cache.setter
1697 1691 def changeset_cache(self, val):
1698 1692 try:
1699 1693 self._changeset_cache = json.dumps(val)
1700 1694 except Exception:
1701 1695 log.error(traceback.format_exc())
1702 1696
1703 1697 @hybrid_property
1704 1698 def repo_name(self):
1705 1699 return self._repo_name
1706 1700
1707 1701 @repo_name.setter
1708 1702 def repo_name(self, value):
1709 1703 self._repo_name = value
1710 1704 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1711 1705
1712 1706 @classmethod
1713 1707 def normalize_repo_name(cls, repo_name):
1714 1708 """
1715 1709 Normalizes os specific repo_name to the format internally stored inside
1716 1710 database using URL_SEP
1717 1711
1718 1712 :param cls:
1719 1713 :param repo_name:
1720 1714 """
1721 1715 return cls.NAME_SEP.join(repo_name.split(os.sep))
1722 1716
1723 1717 @classmethod
1724 1718 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1725 1719 session = Session()
1726 1720 q = session.query(cls).filter(cls.repo_name == repo_name)
1727 1721
1728 1722 if cache:
1729 1723 if identity_cache:
1730 1724 val = cls.identity_cache(session, 'repo_name', repo_name)
1731 1725 if val:
1732 1726 return val
1733 1727 else:
1734 1728 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1735 1729 q = q.options(
1736 1730 FromCache("sql_cache_short", cache_key))
1737 1731
1738 1732 return q.scalar()
1739 1733
1740 1734 @classmethod
1741 1735 def get_by_id_or_repo_name(cls, repoid):
1742 1736 if isinstance(repoid, (int, long)):
1743 1737 try:
1744 1738 repo = cls.get(repoid)
1745 1739 except ValueError:
1746 1740 repo = None
1747 1741 else:
1748 1742 repo = cls.get_by_repo_name(repoid)
1749 1743 return repo
1750 1744
1751 1745 @classmethod
1752 1746 def get_by_full_path(cls, repo_full_path):
1753 1747 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1754 1748 repo_name = cls.normalize_repo_name(repo_name)
1755 1749 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1756 1750
1757 1751 @classmethod
1758 1752 def get_repo_forks(cls, repo_id):
1759 1753 return cls.query().filter(Repository.fork_id == repo_id)
1760 1754
1761 1755 @classmethod
1762 1756 def base_path(cls):
1763 1757 """
1764 1758 Returns base path when all repos are stored
1765 1759
1766 1760 :param cls:
1767 1761 """
1768 1762 q = Session().query(RhodeCodeUi)\
1769 1763 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1770 1764 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1771 1765 return q.one().ui_value
1772 1766
1773 1767 @classmethod
1774 1768 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1775 1769 case_insensitive=True):
1776 1770 q = Repository.query()
1777 1771
1778 1772 if not isinstance(user_id, Optional):
1779 1773 q = q.filter(Repository.user_id == user_id)
1780 1774
1781 1775 if not isinstance(group_id, Optional):
1782 1776 q = q.filter(Repository.group_id == group_id)
1783 1777
1784 1778 if case_insensitive:
1785 1779 q = q.order_by(func.lower(Repository.repo_name))
1786 1780 else:
1787 1781 q = q.order_by(Repository.repo_name)
1788 1782 return q.all()
1789 1783
1790 1784 @property
1791 1785 def forks(self):
1792 1786 """
1793 1787 Return forks of this repo
1794 1788 """
1795 1789 return Repository.get_repo_forks(self.repo_id)
1796 1790
1797 1791 @property
1798 1792 def parent(self):
1799 1793 """
1800 1794 Returns fork parent
1801 1795 """
1802 1796 return self.fork
1803 1797
1804 1798 @property
1805 1799 def just_name(self):
1806 1800 return self.repo_name.split(self.NAME_SEP)[-1]
1807 1801
1808 1802 @property
1809 1803 def groups_with_parents(self):
1810 1804 groups = []
1811 1805 if self.group is None:
1812 1806 return groups
1813 1807
1814 1808 cur_gr = self.group
1815 1809 groups.insert(0, cur_gr)
1816 1810 while 1:
1817 1811 gr = getattr(cur_gr, 'parent_group', None)
1818 1812 cur_gr = cur_gr.parent_group
1819 1813 if gr is None:
1820 1814 break
1821 1815 groups.insert(0, gr)
1822 1816
1823 1817 return groups
1824 1818
1825 1819 @property
1826 1820 def groups_and_repo(self):
1827 1821 return self.groups_with_parents, self
1828 1822
1829 1823 @LazyProperty
1830 1824 def repo_path(self):
1831 1825 """
1832 1826 Returns base full path for that repository means where it actually
1833 1827 exists on a filesystem
1834 1828 """
1835 1829 q = Session().query(RhodeCodeUi).filter(
1836 1830 RhodeCodeUi.ui_key == self.NAME_SEP)
1837 1831 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1838 1832 return q.one().ui_value
1839 1833
1840 1834 @property
1841 1835 def repo_full_path(self):
1842 1836 p = [self.repo_path]
1843 1837 # we need to split the name by / since this is how we store the
1844 1838 # names in the database, but that eventually needs to be converted
1845 1839 # into a valid system path
1846 1840 p += self.repo_name.split(self.NAME_SEP)
1847 1841 return os.path.join(*map(safe_unicode, p))
1848 1842
1849 1843 @property
1850 1844 def cache_keys(self):
1851 1845 """
1852 1846 Returns associated cache keys for that repo
1853 1847 """
1854 1848 return CacheKey.query()\
1855 1849 .filter(CacheKey.cache_args == self.repo_name)\
1856 1850 .order_by(CacheKey.cache_key)\
1857 1851 .all()
1858 1852
1859 1853 @property
1860 1854 def cached_diffs_relative_dir(self):
1861 1855 """
1862 1856 Return a relative to the repository store path of cached diffs
1863 1857 used for safe display for users, who shouldn't know the absolute store
1864 1858 path
1865 1859 """
1866 1860 return os.path.join(
1867 1861 os.path.dirname(self.repo_name),
1868 1862 self.cached_diffs_dir.split(os.path.sep)[-1])
1869 1863
1870 1864 @property
1871 1865 def cached_diffs_dir(self):
1872 1866 path = self.repo_full_path
1873 1867 return os.path.join(
1874 1868 os.path.dirname(path),
1875 1869 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1876 1870
1877 1871 def cached_diffs(self):
1878 1872 diff_cache_dir = self.cached_diffs_dir
1879 1873 if os.path.isdir(diff_cache_dir):
1880 1874 return os.listdir(diff_cache_dir)
1881 1875 return []
1882 1876
1883 1877 def shadow_repos(self):
1884 1878 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
1885 1879 return [
1886 1880 x for x in os.listdir(os.path.dirname(self.repo_full_path))
1887 1881 if x.startswith(shadow_repos_pattern)]
1888 1882
1889 1883 def get_new_name(self, repo_name):
1890 1884 """
1891 1885 returns new full repository name based on assigned group and new new
1892 1886
1893 1887 :param group_name:
1894 1888 """
1895 1889 path_prefix = self.group.full_path_splitted if self.group else []
1896 1890 return self.NAME_SEP.join(path_prefix + [repo_name])
1897 1891
1898 1892 @property
1899 1893 def _config(self):
1900 1894 """
1901 1895 Returns db based config object.
1902 1896 """
1903 1897 from rhodecode.lib.utils import make_db_config
1904 1898 return make_db_config(clear_session=False, repo=self)
1905 1899
1906 1900 def permissions(self, with_admins=True, with_owner=True):
1907 1901 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1908 1902 q = q.options(joinedload(UserRepoToPerm.repository),
1909 1903 joinedload(UserRepoToPerm.user),
1910 1904 joinedload(UserRepoToPerm.permission),)
1911 1905
1912 1906 # get owners and admins and permissions. We do a trick of re-writing
1913 1907 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1914 1908 # has a global reference and changing one object propagates to all
1915 1909 # others. This means if admin is also an owner admin_row that change
1916 1910 # would propagate to both objects
1917 1911 perm_rows = []
1918 1912 for _usr in q.all():
1919 1913 usr = AttributeDict(_usr.user.get_dict())
1920 1914 usr.permission = _usr.permission.permission_name
1921 1915 perm_rows.append(usr)
1922 1916
1923 1917 # filter the perm rows by 'default' first and then sort them by
1924 1918 # admin,write,read,none permissions sorted again alphabetically in
1925 1919 # each group
1926 1920 perm_rows = sorted(perm_rows, key=display_user_sort)
1927 1921
1928 1922 _admin_perm = 'repository.admin'
1929 1923 owner_row = []
1930 1924 if with_owner:
1931 1925 usr = AttributeDict(self.user.get_dict())
1932 1926 usr.owner_row = True
1933 1927 usr.permission = _admin_perm
1934 1928 owner_row.append(usr)
1935 1929
1936 1930 super_admin_rows = []
1937 1931 if with_admins:
1938 1932 for usr in User.get_all_super_admins():
1939 1933 # if this admin is also owner, don't double the record
1940 1934 if usr.user_id == owner_row[0].user_id:
1941 1935 owner_row[0].admin_row = True
1942 1936 else:
1943 1937 usr = AttributeDict(usr.get_dict())
1944 1938 usr.admin_row = True
1945 1939 usr.permission = _admin_perm
1946 1940 super_admin_rows.append(usr)
1947 1941
1948 1942 return super_admin_rows + owner_row + perm_rows
1949 1943
1950 1944 def permission_user_groups(self):
1951 1945 q = UserGroupRepoToPerm.query().filter(
1952 1946 UserGroupRepoToPerm.repository == self)
1953 1947 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1954 1948 joinedload(UserGroupRepoToPerm.users_group),
1955 1949 joinedload(UserGroupRepoToPerm.permission),)
1956 1950
1957 1951 perm_rows = []
1958 1952 for _user_group in q.all():
1959 1953 usr = AttributeDict(_user_group.users_group.get_dict())
1960 1954 usr.permission = _user_group.permission.permission_name
1961 1955 perm_rows.append(usr)
1962 1956
1963 1957 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1964 1958 return perm_rows
1965 1959
1966 1960 def get_api_data(self, include_secrets=False):
1967 1961 """
1968 1962 Common function for generating repo api data
1969 1963
1970 1964 :param include_secrets: See :meth:`User.get_api_data`.
1971 1965
1972 1966 """
1973 1967 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1974 1968 # move this methods on models level.
1975 1969 from rhodecode.model.settings import SettingsModel
1976 1970 from rhodecode.model.repo import RepoModel
1977 1971
1978 1972 repo = self
1979 1973 _user_id, _time, _reason = self.locked
1980 1974
1981 1975 data = {
1982 1976 'repo_id': repo.repo_id,
1983 1977 'repo_name': repo.repo_name,
1984 1978 'repo_type': repo.repo_type,
1985 1979 'clone_uri': repo.clone_uri or '',
1986 1980 'push_uri': repo.push_uri or '',
1987 1981 'url': RepoModel().get_url(self),
1988 1982 'private': repo.private,
1989 1983 'created_on': repo.created_on,
1990 1984 'description': repo.description_safe,
1991 1985 'landing_rev': repo.landing_rev,
1992 1986 'owner': repo.user.username,
1993 1987 'fork_of': repo.fork.repo_name if repo.fork else None,
1994 1988 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1995 1989 'enable_statistics': repo.enable_statistics,
1996 1990 'enable_locking': repo.enable_locking,
1997 1991 'enable_downloads': repo.enable_downloads,
1998 1992 'last_changeset': repo.changeset_cache,
1999 1993 'locked_by': User.get(_user_id).get_api_data(
2000 1994 include_secrets=include_secrets) if _user_id else None,
2001 1995 'locked_date': time_to_datetime(_time) if _time else None,
2002 1996 'lock_reason': _reason if _reason else None,
2003 1997 }
2004 1998
2005 1999 # TODO: mikhail: should be per-repo settings here
2006 2000 rc_config = SettingsModel().get_all_settings()
2007 2001 repository_fields = str2bool(
2008 2002 rc_config.get('rhodecode_repository_fields'))
2009 2003 if repository_fields:
2010 2004 for f in self.extra_fields:
2011 2005 data[f.field_key_prefixed] = f.field_value
2012 2006
2013 2007 return data
2014 2008
2015 2009 @classmethod
2016 2010 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2017 2011 if not lock_time:
2018 2012 lock_time = time.time()
2019 2013 if not lock_reason:
2020 2014 lock_reason = cls.LOCK_AUTOMATIC
2021 2015 repo.locked = [user_id, lock_time, lock_reason]
2022 2016 Session().add(repo)
2023 2017 Session().commit()
2024 2018
2025 2019 @classmethod
2026 2020 def unlock(cls, repo):
2027 2021 repo.locked = None
2028 2022 Session().add(repo)
2029 2023 Session().commit()
2030 2024
2031 2025 @classmethod
2032 2026 def getlock(cls, repo):
2033 2027 return repo.locked
2034 2028
2035 2029 def is_user_lock(self, user_id):
2036 2030 if self.lock[0]:
2037 2031 lock_user_id = safe_int(self.lock[0])
2038 2032 user_id = safe_int(user_id)
2039 2033 # both are ints, and they are equal
2040 2034 return all([lock_user_id, user_id]) and lock_user_id == user_id
2041 2035
2042 2036 return False
2043 2037
2044 2038 def get_locking_state(self, action, user_id, only_when_enabled=True):
2045 2039 """
2046 2040 Checks locking on this repository, if locking is enabled and lock is
2047 2041 present returns a tuple of make_lock, locked, locked_by.
2048 2042 make_lock can have 3 states None (do nothing) True, make lock
2049 2043 False release lock, This value is later propagated to hooks, which
2050 2044 do the locking. Think about this as signals passed to hooks what to do.
2051 2045
2052 2046 """
2053 2047 # TODO: johbo: This is part of the business logic and should be moved
2054 2048 # into the RepositoryModel.
2055 2049
2056 2050 if action not in ('push', 'pull'):
2057 2051 raise ValueError("Invalid action value: %s" % repr(action))
2058 2052
2059 2053 # defines if locked error should be thrown to user
2060 2054 currently_locked = False
2061 2055 # defines if new lock should be made, tri-state
2062 2056 make_lock = None
2063 2057 repo = self
2064 2058 user = User.get(user_id)
2065 2059
2066 2060 lock_info = repo.locked
2067 2061
2068 2062 if repo and (repo.enable_locking or not only_when_enabled):
2069 2063 if action == 'push':
2070 2064 # check if it's already locked !, if it is compare users
2071 2065 locked_by_user_id = lock_info[0]
2072 2066 if user.user_id == locked_by_user_id:
2073 2067 log.debug(
2074 2068 'Got `push` action from user %s, now unlocking', user)
2075 2069 # unlock if we have push from user who locked
2076 2070 make_lock = False
2077 2071 else:
2078 2072 # we're not the same user who locked, ban with
2079 2073 # code defined in settings (default is 423 HTTP Locked) !
2080 2074 log.debug('Repo %s is currently locked by %s', repo, user)
2081 2075 currently_locked = True
2082 2076 elif action == 'pull':
2083 2077 # [0] user [1] date
2084 2078 if lock_info[0] and lock_info[1]:
2085 2079 log.debug('Repo %s is currently locked by %s', repo, user)
2086 2080 currently_locked = True
2087 2081 else:
2088 2082 log.debug('Setting lock on repo %s by %s', repo, user)
2089 2083 make_lock = True
2090 2084
2091 2085 else:
2092 2086 log.debug('Repository %s do not have locking enabled', repo)
2093 2087
2094 2088 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2095 2089 make_lock, currently_locked, lock_info)
2096 2090
2097 2091 from rhodecode.lib.auth import HasRepoPermissionAny
2098 2092 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2099 2093 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2100 2094 # if we don't have at least write permission we cannot make a lock
2101 2095 log.debug('lock state reset back to FALSE due to lack '
2102 2096 'of at least read permission')
2103 2097 make_lock = False
2104 2098
2105 2099 return make_lock, currently_locked, lock_info
2106 2100
2107 2101 @property
2108 2102 def last_db_change(self):
2109 2103 return self.updated_on
2110 2104
2111 2105 @property
2112 2106 def clone_uri_hidden(self):
2113 2107 clone_uri = self.clone_uri
2114 2108 if clone_uri:
2115 2109 import urlobject
2116 2110 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2117 2111 if url_obj.password:
2118 2112 clone_uri = url_obj.with_password('*****')
2119 2113 return clone_uri
2120 2114
2121 2115 @property
2122 2116 def push_uri_hidden(self):
2123 2117 push_uri = self.push_uri
2124 2118 if push_uri:
2125 2119 import urlobject
2126 2120 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2127 2121 if url_obj.password:
2128 2122 push_uri = url_obj.with_password('*****')
2129 2123 return push_uri
2130 2124
2131 2125 def clone_url(self, **override):
2132 2126 from rhodecode.model.settings import SettingsModel
2133 2127
2134 2128 uri_tmpl = None
2135 2129 if 'with_id' in override:
2136 2130 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2137 2131 del override['with_id']
2138 2132
2139 2133 if 'uri_tmpl' in override:
2140 2134 uri_tmpl = override['uri_tmpl']
2141 2135 del override['uri_tmpl']
2142 2136
2143 2137 ssh = False
2144 2138 if 'ssh' in override:
2145 2139 ssh = True
2146 2140 del override['ssh']
2147 2141
2148 2142 # we didn't override our tmpl from **overrides
2149 2143 if not uri_tmpl:
2150 2144 rc_config = SettingsModel().get_all_settings(cache=True)
2151 2145 if ssh:
2152 2146 uri_tmpl = rc_config.get(
2153 2147 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2154 2148 else:
2155 2149 uri_tmpl = rc_config.get(
2156 2150 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2157 2151
2158 2152 request = get_current_request()
2159 2153 return get_clone_url(request=request,
2160 2154 uri_tmpl=uri_tmpl,
2161 2155 repo_name=self.repo_name,
2162 2156 repo_id=self.repo_id, **override)
2163 2157
2164 2158 def set_state(self, state):
2165 2159 self.repo_state = state
2166 2160 Session().add(self)
2167 2161 #==========================================================================
2168 2162 # SCM PROPERTIES
2169 2163 #==========================================================================
2170 2164
2171 2165 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
2172 2166 return get_commit_safe(
2173 2167 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
2174 2168
2175 2169 def get_changeset(self, rev=None, pre_load=None):
2176 2170 warnings.warn("Use get_commit", DeprecationWarning)
2177 2171 commit_id = None
2178 2172 commit_idx = None
2179 2173 if isinstance(rev, basestring):
2180 2174 commit_id = rev
2181 2175 else:
2182 2176 commit_idx = rev
2183 2177 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2184 2178 pre_load=pre_load)
2185 2179
2186 2180 def get_landing_commit(self):
2187 2181 """
2188 2182 Returns landing commit, or if that doesn't exist returns the tip
2189 2183 """
2190 2184 _rev_type, _rev = self.landing_rev
2191 2185 commit = self.get_commit(_rev)
2192 2186 if isinstance(commit, EmptyCommit):
2193 2187 return self.get_commit()
2194 2188 return commit
2195 2189
2196 2190 def update_commit_cache(self, cs_cache=None, config=None):
2197 2191 """
2198 2192 Update cache of last changeset for repository, keys should be::
2199 2193
2200 2194 short_id
2201 2195 raw_id
2202 2196 revision
2203 2197 parents
2204 2198 message
2205 2199 date
2206 2200 author
2207 2201
2208 2202 :param cs_cache:
2209 2203 """
2210 2204 from rhodecode.lib.vcs.backends.base import BaseChangeset
2211 2205 if cs_cache is None:
2212 2206 # use no-cache version here
2213 2207 scm_repo = self.scm_instance(cache=False, config=config)
2214 2208 if scm_repo:
2215 2209 cs_cache = scm_repo.get_commit(
2216 2210 pre_load=["author", "date", "message", "parents"])
2217 2211 else:
2218 2212 cs_cache = EmptyCommit()
2219 2213
2220 2214 if isinstance(cs_cache, BaseChangeset):
2221 2215 cs_cache = cs_cache.__json__()
2222 2216
2223 2217 def is_outdated(new_cs_cache):
2224 2218 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2225 2219 new_cs_cache['revision'] != self.changeset_cache['revision']):
2226 2220 return True
2227 2221 return False
2228 2222
2229 2223 # check if we have maybe already latest cached revision
2230 2224 if is_outdated(cs_cache) or not self.changeset_cache:
2231 2225 _default = datetime.datetime.utcnow()
2232 2226 last_change = cs_cache.get('date') or _default
2233 2227 if self.updated_on and self.updated_on > last_change:
2234 2228 # we check if last update is newer than the new value
2235 2229 # if yes, we use the current timestamp instead. Imagine you get
2236 2230 # old commit pushed 1y ago, we'd set last update 1y to ago.
2237 2231 last_change = _default
2238 2232 log.debug('updated repo %s with new cs cache %s',
2239 2233 self.repo_name, cs_cache)
2240 2234 self.updated_on = last_change
2241 2235 self.changeset_cache = cs_cache
2242 2236 Session().add(self)
2243 2237 Session().commit()
2244 2238 else:
2245 2239 log.debug('Skipping update_commit_cache for repo:`%s` '
2246 2240 'commit already with latest changes', self.repo_name)
2247 2241
2248 2242 @property
2249 2243 def tip(self):
2250 2244 return self.get_commit('tip')
2251 2245
2252 2246 @property
2253 2247 def author(self):
2254 2248 return self.tip.author
2255 2249
2256 2250 @property
2257 2251 def last_change(self):
2258 2252 return self.scm_instance().last_change
2259 2253
2260 2254 def get_comments(self, revisions=None):
2261 2255 """
2262 2256 Returns comments for this repository grouped by revisions
2263 2257
2264 2258 :param revisions: filter query by revisions only
2265 2259 """
2266 2260 cmts = ChangesetComment.query()\
2267 2261 .filter(ChangesetComment.repo == self)
2268 2262 if revisions:
2269 2263 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2270 2264 grouped = collections.defaultdict(list)
2271 2265 for cmt in cmts.all():
2272 2266 grouped[cmt.revision].append(cmt)
2273 2267 return grouped
2274 2268
2275 2269 def statuses(self, revisions=None):
2276 2270 """
2277 2271 Returns statuses for this repository
2278 2272
2279 2273 :param revisions: list of revisions to get statuses for
2280 2274 """
2281 2275 statuses = ChangesetStatus.query()\
2282 2276 .filter(ChangesetStatus.repo == self)\
2283 2277 .filter(ChangesetStatus.version == 0)
2284 2278
2285 2279 if revisions:
2286 2280 # Try doing the filtering in chunks to avoid hitting limits
2287 2281 size = 500
2288 2282 status_results = []
2289 2283 for chunk in xrange(0, len(revisions), size):
2290 2284 status_results += statuses.filter(
2291 2285 ChangesetStatus.revision.in_(
2292 2286 revisions[chunk: chunk+size])
2293 2287 ).all()
2294 2288 else:
2295 2289 status_results = statuses.all()
2296 2290
2297 2291 grouped = {}
2298 2292
2299 2293 # maybe we have open new pullrequest without a status?
2300 2294 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2301 2295 status_lbl = ChangesetStatus.get_status_lbl(stat)
2302 2296 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2303 2297 for rev in pr.revisions:
2304 2298 pr_id = pr.pull_request_id
2305 2299 pr_repo = pr.target_repo.repo_name
2306 2300 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2307 2301
2308 2302 for stat in status_results:
2309 2303 pr_id = pr_repo = None
2310 2304 if stat.pull_request:
2311 2305 pr_id = stat.pull_request.pull_request_id
2312 2306 pr_repo = stat.pull_request.target_repo.repo_name
2313 2307 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2314 2308 pr_id, pr_repo]
2315 2309 return grouped
2316 2310
2317 2311 # ==========================================================================
2318 2312 # SCM CACHE INSTANCE
2319 2313 # ==========================================================================
2320 2314
2321 2315 def scm_instance(self, **kwargs):
2322 2316 import rhodecode
2323 2317
2324 2318 # Passing a config will not hit the cache currently only used
2325 2319 # for repo2dbmapper
2326 2320 config = kwargs.pop('config', None)
2327 2321 cache = kwargs.pop('cache', None)
2328 2322 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2329 2323 # if cache is NOT defined use default global, else we have a full
2330 2324 # control over cache behaviour
2331 2325 if cache is None and full_cache and not config:
2332 2326 return self._get_instance_cached()
2333 2327 return self._get_instance(cache=bool(cache), config=config)
2334 2328
2335 2329 def _get_instance_cached(self):
2336 2330 @cache_region('long_term')
2337 2331 def _get_repo(cache_key):
2338 2332 return self._get_instance()
2339 2333
2340 2334 invalidator_context = CacheKey.repo_context_cache(
2341 2335 _get_repo, self.repo_name, None, thread_scoped=True)
2342 2336
2343 2337 with invalidator_context as context:
2344 2338 context.invalidate()
2345 2339 repo = context.compute()
2346 2340
2347 2341 return repo
2348 2342
2349 2343 def _get_instance(self, cache=True, config=None):
2350 2344 config = config or self._config
2351 2345 custom_wire = {
2352 2346 'cache': cache # controls the vcs.remote cache
2353 2347 }
2354 2348 repo = get_vcs_instance(
2355 2349 repo_path=safe_str(self.repo_full_path),
2356 2350 config=config,
2357 2351 with_wire=custom_wire,
2358 2352 create=False,
2359 2353 _vcs_alias=self.repo_type)
2360 2354
2361 2355 return repo
2362 2356
2363 2357 def __json__(self):
2364 2358 return {'landing_rev': self.landing_rev}
2365 2359
2366 2360 def get_dict(self):
2367 2361
2368 2362 # Since we transformed `repo_name` to a hybrid property, we need to
2369 2363 # keep compatibility with the code which uses `repo_name` field.
2370 2364
2371 2365 result = super(Repository, self).get_dict()
2372 2366 result['repo_name'] = result.pop('_repo_name', None)
2373 2367 return result
2374 2368
2375 2369
2376 2370 class RepoGroup(Base, BaseModel):
2377 2371 __tablename__ = 'groups'
2378 2372 __table_args__ = (
2379 2373 UniqueConstraint('group_name', 'group_parent_id'),
2380 2374 CheckConstraint('group_id != group_parent_id'),
2381 2375 base_table_args,
2382 2376 )
2383 2377 __mapper_args__ = {'order_by': 'group_name'}
2384 2378
2385 2379 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2386 2380
2387 2381 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2388 2382 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2389 2383 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2390 2384 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2391 2385 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2392 2386 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2393 2387 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2394 2388 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2395 2389 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2396 2390
2397 2391 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2398 2392 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2399 2393 parent_group = relationship('RepoGroup', remote_side=group_id)
2400 2394 user = relationship('User')
2401 2395 integrations = relationship('Integration',
2402 2396 cascade="all, delete, delete-orphan")
2403 2397
2404 2398 def __init__(self, group_name='', parent_group=None):
2405 2399 self.group_name = group_name
2406 2400 self.parent_group = parent_group
2407 2401
2408 2402 def __unicode__(self):
2409 2403 return u"<%s('id:%s:%s')>" % (
2410 2404 self.__class__.__name__, self.group_id, self.group_name)
2411 2405
2412 2406 @hybrid_property
2413 2407 def description_safe(self):
2414 2408 from rhodecode.lib import helpers as h
2415 2409 return h.escape(self.group_description)
2416 2410
2417 2411 @classmethod
2418 2412 def _generate_choice(cls, repo_group):
2419 2413 from webhelpers.html import literal as _literal
2420 2414 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2421 2415 return repo_group.group_id, _name(repo_group.full_path_splitted)
2422 2416
2423 2417 @classmethod
2424 2418 def groups_choices(cls, groups=None, show_empty_group=True):
2425 2419 if not groups:
2426 2420 groups = cls.query().all()
2427 2421
2428 2422 repo_groups = []
2429 2423 if show_empty_group:
2430 2424 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2431 2425
2432 2426 repo_groups.extend([cls._generate_choice(x) for x in groups])
2433 2427
2434 2428 repo_groups = sorted(
2435 2429 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2436 2430 return repo_groups
2437 2431
2438 2432 @classmethod
2439 2433 def url_sep(cls):
2440 2434 return URL_SEP
2441 2435
2442 2436 @classmethod
2443 2437 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2444 2438 if case_insensitive:
2445 2439 gr = cls.query().filter(func.lower(cls.group_name)
2446 2440 == func.lower(group_name))
2447 2441 else:
2448 2442 gr = cls.query().filter(cls.group_name == group_name)
2449 2443 if cache:
2450 2444 name_key = _hash_key(group_name)
2451 2445 gr = gr.options(
2452 2446 FromCache("sql_cache_short", "get_group_%s" % name_key))
2453 2447 return gr.scalar()
2454 2448
2455 2449 @classmethod
2456 2450 def get_user_personal_repo_group(cls, user_id):
2457 2451 user = User.get(user_id)
2458 2452 if user.username == User.DEFAULT_USER:
2459 2453 return None
2460 2454
2461 2455 return cls.query()\
2462 2456 .filter(cls.personal == true()) \
2463 2457 .filter(cls.user == user).scalar()
2464 2458
2465 2459 @classmethod
2466 2460 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2467 2461 case_insensitive=True):
2468 2462 q = RepoGroup.query()
2469 2463
2470 2464 if not isinstance(user_id, Optional):
2471 2465 q = q.filter(RepoGroup.user_id == user_id)
2472 2466
2473 2467 if not isinstance(group_id, Optional):
2474 2468 q = q.filter(RepoGroup.group_parent_id == group_id)
2475 2469
2476 2470 if case_insensitive:
2477 2471 q = q.order_by(func.lower(RepoGroup.group_name))
2478 2472 else:
2479 2473 q = q.order_by(RepoGroup.group_name)
2480 2474 return q.all()
2481 2475
2482 2476 @property
2483 2477 def parents(self):
2484 2478 parents_recursion_limit = 10
2485 2479 groups = []
2486 2480 if self.parent_group is None:
2487 2481 return groups
2488 2482 cur_gr = self.parent_group
2489 2483 groups.insert(0, cur_gr)
2490 2484 cnt = 0
2491 2485 while 1:
2492 2486 cnt += 1
2493 2487 gr = getattr(cur_gr, 'parent_group', None)
2494 2488 cur_gr = cur_gr.parent_group
2495 2489 if gr is None:
2496 2490 break
2497 2491 if cnt == parents_recursion_limit:
2498 2492 # this will prevent accidental infinit loops
2499 2493 log.error(('more than %s parents found for group %s, stopping '
2500 2494 'recursive parent fetching' % (parents_recursion_limit, self)))
2501 2495 break
2502 2496
2503 2497 groups.insert(0, gr)
2504 2498 return groups
2505 2499
2506 2500 @property
2507 2501 def last_db_change(self):
2508 2502 return self.updated_on
2509 2503
2510 2504 @property
2511 2505 def children(self):
2512 2506 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2513 2507
2514 2508 @property
2515 2509 def name(self):
2516 2510 return self.group_name.split(RepoGroup.url_sep())[-1]
2517 2511
2518 2512 @property
2519 2513 def full_path(self):
2520 2514 return self.group_name
2521 2515
2522 2516 @property
2523 2517 def full_path_splitted(self):
2524 2518 return self.group_name.split(RepoGroup.url_sep())
2525 2519
2526 2520 @property
2527 2521 def repositories(self):
2528 2522 return Repository.query()\
2529 2523 .filter(Repository.group == self)\
2530 2524 .order_by(Repository.repo_name)
2531 2525
2532 2526 @property
2533 2527 def repositories_recursive_count(self):
2534 2528 cnt = self.repositories.count()
2535 2529
2536 2530 def children_count(group):
2537 2531 cnt = 0
2538 2532 for child in group.children:
2539 2533 cnt += child.repositories.count()
2540 2534 cnt += children_count(child)
2541 2535 return cnt
2542 2536
2543 2537 return cnt + children_count(self)
2544 2538
2545 2539 def _recursive_objects(self, include_repos=True):
2546 2540 all_ = []
2547 2541
2548 2542 def _get_members(root_gr):
2549 2543 if include_repos:
2550 2544 for r in root_gr.repositories:
2551 2545 all_.append(r)
2552 2546 childs = root_gr.children.all()
2553 2547 if childs:
2554 2548 for gr in childs:
2555 2549 all_.append(gr)
2556 2550 _get_members(gr)
2557 2551
2558 2552 _get_members(self)
2559 2553 return [self] + all_
2560 2554
2561 2555 def recursive_groups_and_repos(self):
2562 2556 """
2563 2557 Recursive return all groups, with repositories in those groups
2564 2558 """
2565 2559 return self._recursive_objects()
2566 2560
2567 2561 def recursive_groups(self):
2568 2562 """
2569 2563 Returns all children groups for this group including children of children
2570 2564 """
2571 2565 return self._recursive_objects(include_repos=False)
2572 2566
2573 2567 def get_new_name(self, group_name):
2574 2568 """
2575 2569 returns new full group name based on parent and new name
2576 2570
2577 2571 :param group_name:
2578 2572 """
2579 2573 path_prefix = (self.parent_group.full_path_splitted if
2580 2574 self.parent_group else [])
2581 2575 return RepoGroup.url_sep().join(path_prefix + [group_name])
2582 2576
2583 2577 def permissions(self, with_admins=True, with_owner=True):
2584 2578 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2585 2579 q = q.options(joinedload(UserRepoGroupToPerm.group),
2586 2580 joinedload(UserRepoGroupToPerm.user),
2587 2581 joinedload(UserRepoGroupToPerm.permission),)
2588 2582
2589 2583 # get owners and admins and permissions. We do a trick of re-writing
2590 2584 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2591 2585 # has a global reference and changing one object propagates to all
2592 2586 # others. This means if admin is also an owner admin_row that change
2593 2587 # would propagate to both objects
2594 2588 perm_rows = []
2595 2589 for _usr in q.all():
2596 2590 usr = AttributeDict(_usr.user.get_dict())
2597 2591 usr.permission = _usr.permission.permission_name
2598 2592 perm_rows.append(usr)
2599 2593
2600 2594 # filter the perm rows by 'default' first and then sort them by
2601 2595 # admin,write,read,none permissions sorted again alphabetically in
2602 2596 # each group
2603 2597 perm_rows = sorted(perm_rows, key=display_user_sort)
2604 2598
2605 2599 _admin_perm = 'group.admin'
2606 2600 owner_row = []
2607 2601 if with_owner:
2608 2602 usr = AttributeDict(self.user.get_dict())
2609 2603 usr.owner_row = True
2610 2604 usr.permission = _admin_perm
2611 2605 owner_row.append(usr)
2612 2606
2613 2607 super_admin_rows = []
2614 2608 if with_admins:
2615 2609 for usr in User.get_all_super_admins():
2616 2610 # if this admin is also owner, don't double the record
2617 2611 if usr.user_id == owner_row[0].user_id:
2618 2612 owner_row[0].admin_row = True
2619 2613 else:
2620 2614 usr = AttributeDict(usr.get_dict())
2621 2615 usr.admin_row = True
2622 2616 usr.permission = _admin_perm
2623 2617 super_admin_rows.append(usr)
2624 2618
2625 2619 return super_admin_rows + owner_row + perm_rows
2626 2620
2627 2621 def permission_user_groups(self):
2628 2622 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2629 2623 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2630 2624 joinedload(UserGroupRepoGroupToPerm.users_group),
2631 2625 joinedload(UserGroupRepoGroupToPerm.permission),)
2632 2626
2633 2627 perm_rows = []
2634 2628 for _user_group in q.all():
2635 2629 usr = AttributeDict(_user_group.users_group.get_dict())
2636 2630 usr.permission = _user_group.permission.permission_name
2637 2631 perm_rows.append(usr)
2638 2632
2639 2633 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2640 2634 return perm_rows
2641 2635
2642 2636 def get_api_data(self):
2643 2637 """
2644 2638 Common function for generating api data
2645 2639
2646 2640 """
2647 2641 group = self
2648 2642 data = {
2649 2643 'group_id': group.group_id,
2650 2644 'group_name': group.group_name,
2651 2645 'group_description': group.description_safe,
2652 2646 'parent_group': group.parent_group.group_name if group.parent_group else None,
2653 2647 'repositories': [x.repo_name for x in group.repositories],
2654 2648 'owner': group.user.username,
2655 2649 }
2656 2650 return data
2657 2651
2658 2652
2659 2653 class Permission(Base, BaseModel):
2660 2654 __tablename__ = 'permissions'
2661 2655 __table_args__ = (
2662 2656 Index('p_perm_name_idx', 'permission_name'),
2663 2657 base_table_args,
2664 2658 )
2665 2659
2666 2660 PERMS = [
2667 2661 ('hg.admin', _('RhodeCode Super Administrator')),
2668 2662
2669 2663 ('repository.none', _('Repository no access')),
2670 2664 ('repository.read', _('Repository read access')),
2671 2665 ('repository.write', _('Repository write access')),
2672 2666 ('repository.admin', _('Repository admin access')),
2673 2667
2674 2668 ('group.none', _('Repository group no access')),
2675 2669 ('group.read', _('Repository group read access')),
2676 2670 ('group.write', _('Repository group write access')),
2677 2671 ('group.admin', _('Repository group admin access')),
2678 2672
2679 2673 ('usergroup.none', _('User group no access')),
2680 2674 ('usergroup.read', _('User group read access')),
2681 2675 ('usergroup.write', _('User group write access')),
2682 2676 ('usergroup.admin', _('User group admin access')),
2683 2677
2684 2678 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2685 2679 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2686 2680
2687 2681 ('hg.usergroup.create.false', _('User Group creation disabled')),
2688 2682 ('hg.usergroup.create.true', _('User Group creation enabled')),
2689 2683
2690 2684 ('hg.create.none', _('Repository creation disabled')),
2691 2685 ('hg.create.repository', _('Repository creation enabled')),
2692 2686 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2693 2687 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2694 2688
2695 2689 ('hg.fork.none', _('Repository forking disabled')),
2696 2690 ('hg.fork.repository', _('Repository forking enabled')),
2697 2691
2698 2692 ('hg.register.none', _('Registration disabled')),
2699 2693 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2700 2694 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2701 2695
2702 2696 ('hg.password_reset.enabled', _('Password reset enabled')),
2703 2697 ('hg.password_reset.hidden', _('Password reset hidden')),
2704 2698 ('hg.password_reset.disabled', _('Password reset disabled')),
2705 2699
2706 2700 ('hg.extern_activate.manual', _('Manual activation of external account')),
2707 2701 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2708 2702
2709 2703 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2710 2704 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2711 2705 ]
2712 2706
2713 2707 # definition of system default permissions for DEFAULT user
2714 2708 DEFAULT_USER_PERMISSIONS = [
2715 2709 'repository.read',
2716 2710 'group.read',
2717 2711 'usergroup.read',
2718 2712 'hg.create.repository',
2719 2713 'hg.repogroup.create.false',
2720 2714 'hg.usergroup.create.false',
2721 2715 'hg.create.write_on_repogroup.true',
2722 2716 'hg.fork.repository',
2723 2717 'hg.register.manual_activate',
2724 2718 'hg.password_reset.enabled',
2725 2719 'hg.extern_activate.auto',
2726 2720 'hg.inherit_default_perms.true',
2727 2721 ]
2728 2722
2729 2723 # defines which permissions are more important higher the more important
2730 2724 # Weight defines which permissions are more important.
2731 2725 # The higher number the more important.
2732 2726 PERM_WEIGHTS = {
2733 2727 'repository.none': 0,
2734 2728 'repository.read': 1,
2735 2729 'repository.write': 3,
2736 2730 'repository.admin': 4,
2737 2731
2738 2732 'group.none': 0,
2739 2733 'group.read': 1,
2740 2734 'group.write': 3,
2741 2735 'group.admin': 4,
2742 2736
2743 2737 'usergroup.none': 0,
2744 2738 'usergroup.read': 1,
2745 2739 'usergroup.write': 3,
2746 2740 'usergroup.admin': 4,
2747 2741
2748 2742 'hg.repogroup.create.false': 0,
2749 2743 'hg.repogroup.create.true': 1,
2750 2744
2751 2745 'hg.usergroup.create.false': 0,
2752 2746 'hg.usergroup.create.true': 1,
2753 2747
2754 2748 'hg.fork.none': 0,
2755 2749 'hg.fork.repository': 1,
2756 2750 'hg.create.none': 0,
2757 2751 'hg.create.repository': 1
2758 2752 }
2759 2753
2760 2754 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 2755 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2762 2756 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2763 2757
2764 2758 def __unicode__(self):
2765 2759 return u"<%s('%s:%s')>" % (
2766 2760 self.__class__.__name__, self.permission_id, self.permission_name
2767 2761 )
2768 2762
2769 2763 @classmethod
2770 2764 def get_by_key(cls, key):
2771 2765 return cls.query().filter(cls.permission_name == key).scalar()
2772 2766
2773 2767 @classmethod
2774 2768 def get_default_repo_perms(cls, user_id, repo_id=None):
2775 2769 q = Session().query(UserRepoToPerm, Repository, Permission)\
2776 2770 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2777 2771 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2778 2772 .filter(UserRepoToPerm.user_id == user_id)
2779 2773 if repo_id:
2780 2774 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2781 2775 return q.all()
2782 2776
2783 2777 @classmethod
2784 2778 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2785 2779 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2786 2780 .join(
2787 2781 Permission,
2788 2782 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2789 2783 .join(
2790 2784 Repository,
2791 2785 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2792 2786 .join(
2793 2787 UserGroup,
2794 2788 UserGroupRepoToPerm.users_group_id ==
2795 2789 UserGroup.users_group_id)\
2796 2790 .join(
2797 2791 UserGroupMember,
2798 2792 UserGroupRepoToPerm.users_group_id ==
2799 2793 UserGroupMember.users_group_id)\
2800 2794 .filter(
2801 2795 UserGroupMember.user_id == user_id,
2802 2796 UserGroup.users_group_active == true())
2803 2797 if repo_id:
2804 2798 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2805 2799 return q.all()
2806 2800
2807 2801 @classmethod
2808 2802 def get_default_group_perms(cls, user_id, repo_group_id=None):
2809 2803 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2810 2804 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2811 2805 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2812 2806 .filter(UserRepoGroupToPerm.user_id == user_id)
2813 2807 if repo_group_id:
2814 2808 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2815 2809 return q.all()
2816 2810
2817 2811 @classmethod
2818 2812 def get_default_group_perms_from_user_group(
2819 2813 cls, user_id, repo_group_id=None):
2820 2814 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2821 2815 .join(
2822 2816 Permission,
2823 2817 UserGroupRepoGroupToPerm.permission_id ==
2824 2818 Permission.permission_id)\
2825 2819 .join(
2826 2820 RepoGroup,
2827 2821 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2828 2822 .join(
2829 2823 UserGroup,
2830 2824 UserGroupRepoGroupToPerm.users_group_id ==
2831 2825 UserGroup.users_group_id)\
2832 2826 .join(
2833 2827 UserGroupMember,
2834 2828 UserGroupRepoGroupToPerm.users_group_id ==
2835 2829 UserGroupMember.users_group_id)\
2836 2830 .filter(
2837 2831 UserGroupMember.user_id == user_id,
2838 2832 UserGroup.users_group_active == true())
2839 2833 if repo_group_id:
2840 2834 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2841 2835 return q.all()
2842 2836
2843 2837 @classmethod
2844 2838 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2845 2839 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2846 2840 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2847 2841 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2848 2842 .filter(UserUserGroupToPerm.user_id == user_id)
2849 2843 if user_group_id:
2850 2844 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2851 2845 return q.all()
2852 2846
2853 2847 @classmethod
2854 2848 def get_default_user_group_perms_from_user_group(
2855 2849 cls, user_id, user_group_id=None):
2856 2850 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2857 2851 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2858 2852 .join(
2859 2853 Permission,
2860 2854 UserGroupUserGroupToPerm.permission_id ==
2861 2855 Permission.permission_id)\
2862 2856 .join(
2863 2857 TargetUserGroup,
2864 2858 UserGroupUserGroupToPerm.target_user_group_id ==
2865 2859 TargetUserGroup.users_group_id)\
2866 2860 .join(
2867 2861 UserGroup,
2868 2862 UserGroupUserGroupToPerm.user_group_id ==
2869 2863 UserGroup.users_group_id)\
2870 2864 .join(
2871 2865 UserGroupMember,
2872 2866 UserGroupUserGroupToPerm.user_group_id ==
2873 2867 UserGroupMember.users_group_id)\
2874 2868 .filter(
2875 2869 UserGroupMember.user_id == user_id,
2876 2870 UserGroup.users_group_active == true())
2877 2871 if user_group_id:
2878 2872 q = q.filter(
2879 2873 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2880 2874
2881 2875 return q.all()
2882 2876
2883 2877
2884 2878 class UserRepoToPerm(Base, BaseModel):
2885 2879 __tablename__ = 'repo_to_perm'
2886 2880 __table_args__ = (
2887 2881 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2888 2882 base_table_args
2889 2883 )
2890 2884
2891 2885 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2892 2886 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2893 2887 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2894 2888 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2895 2889
2896 2890 user = relationship('User')
2897 2891 repository = relationship('Repository')
2898 2892 permission = relationship('Permission')
2899 2893
2900 2894 @classmethod
2901 2895 def create(cls, user, repository, permission):
2902 2896 n = cls()
2903 2897 n.user = user
2904 2898 n.repository = repository
2905 2899 n.permission = permission
2906 2900 Session().add(n)
2907 2901 return n
2908 2902
2909 2903 def __unicode__(self):
2910 2904 return u'<%s => %s >' % (self.user, self.repository)
2911 2905
2912 2906
2913 2907 class UserUserGroupToPerm(Base, BaseModel):
2914 2908 __tablename__ = 'user_user_group_to_perm'
2915 2909 __table_args__ = (
2916 2910 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2917 2911 base_table_args
2918 2912 )
2919 2913
2920 2914 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2921 2915 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2922 2916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2923 2917 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2924 2918
2925 2919 user = relationship('User')
2926 2920 user_group = relationship('UserGroup')
2927 2921 permission = relationship('Permission')
2928 2922
2929 2923 @classmethod
2930 2924 def create(cls, user, user_group, permission):
2931 2925 n = cls()
2932 2926 n.user = user
2933 2927 n.user_group = user_group
2934 2928 n.permission = permission
2935 2929 Session().add(n)
2936 2930 return n
2937 2931
2938 2932 def __unicode__(self):
2939 2933 return u'<%s => %s >' % (self.user, self.user_group)
2940 2934
2941 2935
2942 2936 class UserToPerm(Base, BaseModel):
2943 2937 __tablename__ = 'user_to_perm'
2944 2938 __table_args__ = (
2945 2939 UniqueConstraint('user_id', 'permission_id'),
2946 2940 base_table_args
2947 2941 )
2948 2942
2949 2943 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2950 2944 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2951 2945 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2952 2946
2953 2947 user = relationship('User')
2954 2948 permission = relationship('Permission', lazy='joined')
2955 2949
2956 2950 def __unicode__(self):
2957 2951 return u'<%s => %s >' % (self.user, self.permission)
2958 2952
2959 2953
2960 2954 class UserGroupRepoToPerm(Base, BaseModel):
2961 2955 __tablename__ = 'users_group_repo_to_perm'
2962 2956 __table_args__ = (
2963 2957 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2964 2958 base_table_args
2965 2959 )
2966 2960
2967 2961 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2968 2962 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2969 2963 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2970 2964 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2971 2965
2972 2966 users_group = relationship('UserGroup')
2973 2967 permission = relationship('Permission')
2974 2968 repository = relationship('Repository')
2975 2969
2976 2970 @classmethod
2977 2971 def create(cls, users_group, repository, permission):
2978 2972 n = cls()
2979 2973 n.users_group = users_group
2980 2974 n.repository = repository
2981 2975 n.permission = permission
2982 2976 Session().add(n)
2983 2977 return n
2984 2978
2985 2979 def __unicode__(self):
2986 2980 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2987 2981
2988 2982
2989 2983 class UserGroupUserGroupToPerm(Base, BaseModel):
2990 2984 __tablename__ = 'user_group_user_group_to_perm'
2991 2985 __table_args__ = (
2992 2986 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2993 2987 CheckConstraint('target_user_group_id != user_group_id'),
2994 2988 base_table_args
2995 2989 )
2996 2990
2997 2991 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2998 2992 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2999 2993 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3000 2994 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3001 2995
3002 2996 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3003 2997 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3004 2998 permission = relationship('Permission')
3005 2999
3006 3000 @classmethod
3007 3001 def create(cls, target_user_group, user_group, permission):
3008 3002 n = cls()
3009 3003 n.target_user_group = target_user_group
3010 3004 n.user_group = user_group
3011 3005 n.permission = permission
3012 3006 Session().add(n)
3013 3007 return n
3014 3008
3015 3009 def __unicode__(self):
3016 3010 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3017 3011
3018 3012
3019 3013 class UserGroupToPerm(Base, BaseModel):
3020 3014 __tablename__ = 'users_group_to_perm'
3021 3015 __table_args__ = (
3022 3016 UniqueConstraint('users_group_id', 'permission_id',),
3023 3017 base_table_args
3024 3018 )
3025 3019
3026 3020 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3027 3021 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3028 3022 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3029 3023
3030 3024 users_group = relationship('UserGroup')
3031 3025 permission = relationship('Permission')
3032 3026
3033 3027
3034 3028 class UserRepoGroupToPerm(Base, BaseModel):
3035 3029 __tablename__ = 'user_repo_group_to_perm'
3036 3030 __table_args__ = (
3037 3031 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3038 3032 base_table_args
3039 3033 )
3040 3034
3041 3035 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3042 3036 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3043 3037 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3044 3038 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3045 3039
3046 3040 user = relationship('User')
3047 3041 group = relationship('RepoGroup')
3048 3042 permission = relationship('Permission')
3049 3043
3050 3044 @classmethod
3051 3045 def create(cls, user, repository_group, permission):
3052 3046 n = cls()
3053 3047 n.user = user
3054 3048 n.group = repository_group
3055 3049 n.permission = permission
3056 3050 Session().add(n)
3057 3051 return n
3058 3052
3059 3053
3060 3054 class UserGroupRepoGroupToPerm(Base, BaseModel):
3061 3055 __tablename__ = 'users_group_repo_group_to_perm'
3062 3056 __table_args__ = (
3063 3057 UniqueConstraint('users_group_id', 'group_id'),
3064 3058 base_table_args
3065 3059 )
3066 3060
3067 3061 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3068 3062 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3069 3063 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3070 3064 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3071 3065
3072 3066 users_group = relationship('UserGroup')
3073 3067 permission = relationship('Permission')
3074 3068 group = relationship('RepoGroup')
3075 3069
3076 3070 @classmethod
3077 3071 def create(cls, user_group, repository_group, permission):
3078 3072 n = cls()
3079 3073 n.users_group = user_group
3080 3074 n.group = repository_group
3081 3075 n.permission = permission
3082 3076 Session().add(n)
3083 3077 return n
3084 3078
3085 3079 def __unicode__(self):
3086 3080 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3087 3081
3088 3082
3089 3083 class Statistics(Base, BaseModel):
3090 3084 __tablename__ = 'statistics'
3091 3085 __table_args__ = (
3092 3086 base_table_args
3093 3087 )
3094 3088
3095 3089 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3096 3090 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3097 3091 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3098 3092 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3099 3093 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3100 3094 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3101 3095
3102 3096 repository = relationship('Repository', single_parent=True)
3103 3097
3104 3098
3105 3099 class UserFollowing(Base, BaseModel):
3106 3100 __tablename__ = 'user_followings'
3107 3101 __table_args__ = (
3108 3102 UniqueConstraint('user_id', 'follows_repository_id'),
3109 3103 UniqueConstraint('user_id', 'follows_user_id'),
3110 3104 base_table_args
3111 3105 )
3112 3106
3113 3107 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3114 3108 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3115 3109 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3116 3110 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3117 3111 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3118 3112
3119 3113 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3120 3114
3121 3115 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3122 3116 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3123 3117
3124 3118 @classmethod
3125 3119 def get_repo_followers(cls, repo_id):
3126 3120 return cls.query().filter(cls.follows_repo_id == repo_id)
3127 3121
3128 3122
3129 3123 class CacheKey(Base, BaseModel):
3130 3124 __tablename__ = 'cache_invalidation'
3131 3125 __table_args__ = (
3132 3126 UniqueConstraint('cache_key'),
3133 3127 Index('key_idx', 'cache_key'),
3134 3128 base_table_args,
3135 3129 )
3136 3130
3137 3131 CACHE_TYPE_ATOM = 'ATOM'
3138 3132 CACHE_TYPE_RSS = 'RSS'
3139 3133 CACHE_TYPE_README = 'README'
3140 3134
3141 3135 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3142 3136 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3143 3137 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3144 3138 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3145 3139
3146 3140 def __init__(self, cache_key, cache_args=''):
3147 3141 self.cache_key = cache_key
3148 3142 self.cache_args = cache_args
3149 3143 self.cache_active = False
3150 3144
3151 3145 def __unicode__(self):
3152 3146 return u"<%s('%s:%s[%s]')>" % (
3153 3147 self.__class__.__name__,
3154 3148 self.cache_id, self.cache_key, self.cache_active)
3155 3149
3156 3150 def _cache_key_partition(self):
3157 3151 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3158 3152 return prefix, repo_name, suffix
3159 3153
3160 3154 def get_prefix(self):
3161 3155 """
3162 3156 Try to extract prefix from existing cache key. The key could consist
3163 3157 of prefix, repo_name, suffix
3164 3158 """
3165 3159 # this returns prefix, repo_name, suffix
3166 3160 return self._cache_key_partition()[0]
3167 3161
3168 3162 def get_suffix(self):
3169 3163 """
3170 3164 get suffix that might have been used in _get_cache_key to
3171 3165 generate self.cache_key. Only used for informational purposes
3172 3166 in repo_edit.mako.
3173 3167 """
3174 3168 # prefix, repo_name, suffix
3175 3169 return self._cache_key_partition()[2]
3176 3170
3177 3171 @classmethod
3178 3172 def delete_all_cache(cls):
3179 3173 """
3180 3174 Delete all cache keys from database.
3181 3175 Should only be run when all instances are down and all entries
3182 3176 thus stale.
3183 3177 """
3184 3178 cls.query().delete()
3185 3179 Session().commit()
3186 3180
3187 3181 @classmethod
3188 3182 def get_cache_key(cls, repo_name, cache_type):
3189 3183 """
3190 3184
3191 3185 Generate a cache key for this process of RhodeCode instance.
3192 3186 Prefix most likely will be process id or maybe explicitly set
3193 3187 instance_id from .ini file.
3194 3188 """
3195 3189 import rhodecode
3196 3190 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3197 3191
3198 3192 repo_as_unicode = safe_unicode(repo_name)
3199 3193 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3200 3194 if cache_type else repo_as_unicode
3201 3195
3202 3196 return u'{}{}'.format(prefix, key)
3203 3197
3204 3198 @classmethod
3205 3199 def set_invalidate(cls, repo_name, delete=False):
3206 3200 """
3207 3201 Mark all caches of a repo as invalid in the database.
3208 3202 """
3209 3203
3210 3204 try:
3211 3205 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3212 3206 if delete:
3213 3207 log.debug('cache objects deleted for repo %s',
3214 3208 safe_str(repo_name))
3215 3209 qry.delete()
3216 3210 else:
3217 3211 log.debug('cache objects marked as invalid for repo %s',
3218 3212 safe_str(repo_name))
3219 3213 qry.update({"cache_active": False})
3220 3214
3221 3215 Session().commit()
3222 3216 except Exception:
3223 3217 log.exception(
3224 3218 'Cache key invalidation failed for repository %s',
3225 3219 safe_str(repo_name))
3226 3220 Session().rollback()
3227 3221
3228 3222 @classmethod
3229 3223 def get_active_cache(cls, cache_key):
3230 3224 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3231 3225 if inv_obj:
3232 3226 return inv_obj
3233 3227 return None
3234 3228
3235 3229 @classmethod
3236 3230 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3237 3231 thread_scoped=False):
3238 3232 """
3239 3233 @cache_region('long_term')
3240 3234 def _heavy_calculation(cache_key):
3241 3235 return 'result'
3242 3236
3243 3237 cache_context = CacheKey.repo_context_cache(
3244 3238 _heavy_calculation, repo_name, cache_type)
3245 3239
3246 3240 with cache_context as context:
3247 3241 context.invalidate()
3248 3242 computed = context.compute()
3249 3243
3250 3244 assert computed == 'result'
3251 3245 """
3252 3246 from rhodecode.lib import caches
3253 3247 return caches.InvalidationContext(
3254 3248 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3255 3249
3256 3250
3257 3251 class ChangesetComment(Base, BaseModel):
3258 3252 __tablename__ = 'changeset_comments'
3259 3253 __table_args__ = (
3260 3254 Index('cc_revision_idx', 'revision'),
3261 3255 base_table_args,
3262 3256 )
3263 3257
3264 3258 COMMENT_OUTDATED = u'comment_outdated'
3265 3259 COMMENT_TYPE_NOTE = u'note'
3266 3260 COMMENT_TYPE_TODO = u'todo'
3267 3261 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3268 3262
3269 3263 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3270 3264 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3271 3265 revision = Column('revision', String(40), nullable=True)
3272 3266 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3273 3267 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3274 3268 line_no = Column('line_no', Unicode(10), nullable=True)
3275 3269 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3276 3270 f_path = Column('f_path', Unicode(1000), nullable=True)
3277 3271 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3278 3272 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3279 3273 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3280 3274 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3281 3275 renderer = Column('renderer', Unicode(64), nullable=True)
3282 3276 display_state = Column('display_state', Unicode(128), nullable=True)
3283 3277
3284 3278 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3285 3279 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3286 3280 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3287 3281 author = relationship('User', lazy='joined')
3288 3282 repo = relationship('Repository')
3289 3283 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3290 3284 pull_request = relationship('PullRequest', lazy='joined')
3291 3285 pull_request_version = relationship('PullRequestVersion')
3292 3286
3293 3287 @classmethod
3294 3288 def get_users(cls, revision=None, pull_request_id=None):
3295 3289 """
3296 3290 Returns user associated with this ChangesetComment. ie those
3297 3291 who actually commented
3298 3292
3299 3293 :param cls:
3300 3294 :param revision:
3301 3295 """
3302 3296 q = Session().query(User)\
3303 3297 .join(ChangesetComment.author)
3304 3298 if revision:
3305 3299 q = q.filter(cls.revision == revision)
3306 3300 elif pull_request_id:
3307 3301 q = q.filter(cls.pull_request_id == pull_request_id)
3308 3302 return q.all()
3309 3303
3310 3304 @classmethod
3311 3305 def get_index_from_version(cls, pr_version, versions):
3312 3306 num_versions = [x.pull_request_version_id for x in versions]
3313 3307 try:
3314 3308 return num_versions.index(pr_version) +1
3315 3309 except (IndexError, ValueError):
3316 3310 return
3317 3311
3318 3312 @property
3319 3313 def outdated(self):
3320 3314 return self.display_state == self.COMMENT_OUTDATED
3321 3315
3322 3316 def outdated_at_version(self, version):
3323 3317 """
3324 3318 Checks if comment is outdated for given pull request version
3325 3319 """
3326 3320 return self.outdated and self.pull_request_version_id != version
3327 3321
3328 3322 def older_than_version(self, version):
3329 3323 """
3330 3324 Checks if comment is made from previous version than given
3331 3325 """
3332 3326 if version is None:
3333 3327 return self.pull_request_version_id is not None
3334 3328
3335 3329 return self.pull_request_version_id < version
3336 3330
3337 3331 @property
3338 3332 def resolved(self):
3339 3333 return self.resolved_by[0] if self.resolved_by else None
3340 3334
3341 3335 @property
3342 3336 def is_todo(self):
3343 3337 return self.comment_type == self.COMMENT_TYPE_TODO
3344 3338
3345 3339 @property
3346 3340 def is_inline(self):
3347 3341 return self.line_no and self.f_path
3348 3342
3349 3343 def get_index_version(self, versions):
3350 3344 return self.get_index_from_version(
3351 3345 self.pull_request_version_id, versions)
3352 3346
3353 3347 def __repr__(self):
3354 3348 if self.comment_id:
3355 3349 return '<DB:Comment #%s>' % self.comment_id
3356 3350 else:
3357 3351 return '<DB:Comment at %#x>' % id(self)
3358 3352
3359 3353 def get_api_data(self):
3360 3354 comment = self
3361 3355 data = {
3362 3356 'comment_id': comment.comment_id,
3363 3357 'comment_type': comment.comment_type,
3364 3358 'comment_text': comment.text,
3365 3359 'comment_status': comment.status_change,
3366 3360 'comment_f_path': comment.f_path,
3367 3361 'comment_lineno': comment.line_no,
3368 3362 'comment_author': comment.author,
3369 3363 'comment_created_on': comment.created_on
3370 3364 }
3371 3365 return data
3372 3366
3373 3367 def __json__(self):
3374 3368 data = dict()
3375 3369 data.update(self.get_api_data())
3376 3370 return data
3377 3371
3378 3372
3379 3373 class ChangesetStatus(Base, BaseModel):
3380 3374 __tablename__ = 'changeset_statuses'
3381 3375 __table_args__ = (
3382 3376 Index('cs_revision_idx', 'revision'),
3383 3377 Index('cs_version_idx', 'version'),
3384 3378 UniqueConstraint('repo_id', 'revision', 'version'),
3385 3379 base_table_args
3386 3380 )
3387 3381
3388 3382 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3389 3383 STATUS_APPROVED = 'approved'
3390 3384 STATUS_REJECTED = 'rejected'
3391 3385 STATUS_UNDER_REVIEW = 'under_review'
3392 3386
3393 3387 STATUSES = [
3394 3388 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3395 3389 (STATUS_APPROVED, _("Approved")),
3396 3390 (STATUS_REJECTED, _("Rejected")),
3397 3391 (STATUS_UNDER_REVIEW, _("Under Review")),
3398 3392 ]
3399 3393
3400 3394 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3401 3395 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3402 3396 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3403 3397 revision = Column('revision', String(40), nullable=False)
3404 3398 status = Column('status', String(128), nullable=False, default=DEFAULT)
3405 3399 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3406 3400 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3407 3401 version = Column('version', Integer(), nullable=False, default=0)
3408 3402 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3409 3403
3410 3404 author = relationship('User', lazy='joined')
3411 3405 repo = relationship('Repository')
3412 3406 comment = relationship('ChangesetComment', lazy='joined')
3413 3407 pull_request = relationship('PullRequest', lazy='joined')
3414 3408
3415 3409 def __unicode__(self):
3416 3410 return u"<%s('%s[v%s]:%s')>" % (
3417 3411 self.__class__.__name__,
3418 3412 self.status, self.version, self.author
3419 3413 )
3420 3414
3421 3415 @classmethod
3422 3416 def get_status_lbl(cls, value):
3423 3417 return dict(cls.STATUSES).get(value)
3424 3418
3425 3419 @property
3426 3420 def status_lbl(self):
3427 3421 return ChangesetStatus.get_status_lbl(self.status)
3428 3422
3429 3423 def get_api_data(self):
3430 3424 status = self
3431 3425 data = {
3432 3426 'status_id': status.changeset_status_id,
3433 3427 'status': status.status,
3434 3428 }
3435 3429 return data
3436 3430
3437 3431 def __json__(self):
3438 3432 data = dict()
3439 3433 data.update(self.get_api_data())
3440 3434 return data
3441 3435
3442 3436
3443 3437 class _PullRequestBase(BaseModel):
3444 3438 """
3445 3439 Common attributes of pull request and version entries.
3446 3440 """
3447 3441
3448 3442 # .status values
3449 3443 STATUS_NEW = u'new'
3450 3444 STATUS_OPEN = u'open'
3451 3445 STATUS_CLOSED = u'closed'
3452 3446
3453 3447 title = Column('title', Unicode(255), nullable=True)
3454 3448 description = Column(
3455 3449 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3456 3450 nullable=True)
3457 3451 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3458 3452
3459 3453 # new/open/closed status of pull request (not approve/reject/etc)
3460 3454 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3461 3455 created_on = Column(
3462 3456 'created_on', DateTime(timezone=False), nullable=False,
3463 3457 default=datetime.datetime.now)
3464 3458 updated_on = Column(
3465 3459 'updated_on', DateTime(timezone=False), nullable=False,
3466 3460 default=datetime.datetime.now)
3467 3461
3468 3462 @declared_attr
3469 3463 def user_id(cls):
3470 3464 return Column(
3471 3465 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3472 3466 unique=None)
3473 3467
3474 3468 # 500 revisions max
3475 3469 _revisions = Column(
3476 3470 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3477 3471
3478 3472 @declared_attr
3479 3473 def source_repo_id(cls):
3480 3474 # TODO: dan: rename column to source_repo_id
3481 3475 return Column(
3482 3476 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3483 3477 nullable=False)
3484 3478
3485 3479 source_ref = Column('org_ref', Unicode(255), nullable=False)
3486 3480
3487 3481 @declared_attr
3488 3482 def target_repo_id(cls):
3489 3483 # TODO: dan: rename column to target_repo_id
3490 3484 return Column(
3491 3485 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3492 3486 nullable=False)
3493 3487
3494 3488 target_ref = Column('other_ref', Unicode(255), nullable=False)
3495 3489 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3496 3490
3497 3491 # TODO: dan: rename column to last_merge_source_rev
3498 3492 _last_merge_source_rev = Column(
3499 3493 'last_merge_org_rev', String(40), nullable=True)
3500 3494 # TODO: dan: rename column to last_merge_target_rev
3501 3495 _last_merge_target_rev = Column(
3502 3496 'last_merge_other_rev', String(40), nullable=True)
3503 3497 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3504 3498 merge_rev = Column('merge_rev', String(40), nullable=True)
3505 3499
3506 3500 reviewer_data = Column(
3507 3501 'reviewer_data_json', MutationObj.as_mutable(
3508 3502 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3509 3503
3510 3504 @property
3511 3505 def reviewer_data_json(self):
3512 3506 return json.dumps(self.reviewer_data)
3513 3507
3514 3508 @hybrid_property
3515 3509 def description_safe(self):
3516 3510 from rhodecode.lib import helpers as h
3517 3511 return h.escape(self.description)
3518 3512
3519 3513 @hybrid_property
3520 3514 def revisions(self):
3521 3515 return self._revisions.split(':') if self._revisions else []
3522 3516
3523 3517 @revisions.setter
3524 3518 def revisions(self, val):
3525 3519 self._revisions = ':'.join(val)
3526 3520
3527 3521 @hybrid_property
3528 3522 def last_merge_status(self):
3529 3523 return safe_int(self._last_merge_status)
3530 3524
3531 3525 @last_merge_status.setter
3532 3526 def last_merge_status(self, val):
3533 3527 self._last_merge_status = val
3534 3528
3535 3529 @declared_attr
3536 3530 def author(cls):
3537 3531 return relationship('User', lazy='joined')
3538 3532
3539 3533 @declared_attr
3540 3534 def source_repo(cls):
3541 3535 return relationship(
3542 3536 'Repository',
3543 3537 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3544 3538
3545 3539 @property
3546 3540 def source_ref_parts(self):
3547 3541 return self.unicode_to_reference(self.source_ref)
3548 3542
3549 3543 @declared_attr
3550 3544 def target_repo(cls):
3551 3545 return relationship(
3552 3546 'Repository',
3553 3547 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3554 3548
3555 3549 @property
3556 3550 def target_ref_parts(self):
3557 3551 return self.unicode_to_reference(self.target_ref)
3558 3552
3559 3553 @property
3560 3554 def shadow_merge_ref(self):
3561 3555 return self.unicode_to_reference(self._shadow_merge_ref)
3562 3556
3563 3557 @shadow_merge_ref.setter
3564 3558 def shadow_merge_ref(self, ref):
3565 3559 self._shadow_merge_ref = self.reference_to_unicode(ref)
3566 3560
3567 3561 def unicode_to_reference(self, raw):
3568 3562 """
3569 3563 Convert a unicode (or string) to a reference object.
3570 3564 If unicode evaluates to False it returns None.
3571 3565 """
3572 3566 if raw:
3573 3567 refs = raw.split(':')
3574 3568 return Reference(*refs)
3575 3569 else:
3576 3570 return None
3577 3571
3578 3572 def reference_to_unicode(self, ref):
3579 3573 """
3580 3574 Convert a reference object to unicode.
3581 3575 If reference is None it returns None.
3582 3576 """
3583 3577 if ref:
3584 3578 return u':'.join(ref)
3585 3579 else:
3586 3580 return None
3587 3581
3588 3582 def get_api_data(self, with_merge_state=True):
3589 3583 from rhodecode.model.pull_request import PullRequestModel
3590 3584
3591 3585 pull_request = self
3592 3586 if with_merge_state:
3593 3587 merge_status = PullRequestModel().merge_status(pull_request)
3594 3588 merge_state = {
3595 3589 'status': merge_status[0],
3596 3590 'message': safe_unicode(merge_status[1]),
3597 3591 }
3598 3592 else:
3599 3593 merge_state = {'status': 'not_available',
3600 3594 'message': 'not_available'}
3601 3595
3602 3596 merge_data = {
3603 3597 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3604 3598 'reference': (
3605 3599 pull_request.shadow_merge_ref._asdict()
3606 3600 if pull_request.shadow_merge_ref else None),
3607 3601 }
3608 3602
3609 3603 data = {
3610 3604 'pull_request_id': pull_request.pull_request_id,
3611 3605 'url': PullRequestModel().get_url(pull_request),
3612 3606 'title': pull_request.title,
3613 3607 'description': pull_request.description,
3614 3608 'status': pull_request.status,
3615 3609 'created_on': pull_request.created_on,
3616 3610 'updated_on': pull_request.updated_on,
3617 3611 'commit_ids': pull_request.revisions,
3618 3612 'review_status': pull_request.calculated_review_status(),
3619 3613 'mergeable': merge_state,
3620 3614 'source': {
3621 3615 'clone_url': pull_request.source_repo.clone_url(),
3622 3616 'repository': pull_request.source_repo.repo_name,
3623 3617 'reference': {
3624 3618 'name': pull_request.source_ref_parts.name,
3625 3619 'type': pull_request.source_ref_parts.type,
3626 3620 'commit_id': pull_request.source_ref_parts.commit_id,
3627 3621 },
3628 3622 },
3629 3623 'target': {
3630 3624 'clone_url': pull_request.target_repo.clone_url(),
3631 3625 'repository': pull_request.target_repo.repo_name,
3632 3626 'reference': {
3633 3627 'name': pull_request.target_ref_parts.name,
3634 3628 'type': pull_request.target_ref_parts.type,
3635 3629 'commit_id': pull_request.target_ref_parts.commit_id,
3636 3630 },
3637 3631 },
3638 3632 'merge': merge_data,
3639 3633 'author': pull_request.author.get_api_data(include_secrets=False,
3640 3634 details='basic'),
3641 3635 'reviewers': [
3642 3636 {
3643 3637 'user': reviewer.get_api_data(include_secrets=False,
3644 3638 details='basic'),
3645 3639 'reasons': reasons,
3646 3640 'review_status': st[0][1].status if st else 'not_reviewed',
3647 3641 }
3648 3642 for obj, reviewer, reasons, mandatory, st in
3649 3643 pull_request.reviewers_statuses()
3650 3644 ]
3651 3645 }
3652 3646
3653 3647 return data
3654 3648
3655 3649
3656 3650 class PullRequest(Base, _PullRequestBase):
3657 3651 __tablename__ = 'pull_requests'
3658 3652 __table_args__ = (
3659 3653 base_table_args,
3660 3654 )
3661 3655
3662 3656 pull_request_id = Column(
3663 3657 'pull_request_id', Integer(), nullable=False, primary_key=True)
3664 3658
3665 3659 def __repr__(self):
3666 3660 if self.pull_request_id:
3667 3661 return '<DB:PullRequest #%s>' % self.pull_request_id
3668 3662 else:
3669 3663 return '<DB:PullRequest at %#x>' % id(self)
3670 3664
3671 3665 reviewers = relationship('PullRequestReviewers',
3672 3666 cascade="all, delete, delete-orphan")
3673 3667 statuses = relationship('ChangesetStatus',
3674 3668 cascade="all, delete, delete-orphan")
3675 3669 comments = relationship('ChangesetComment',
3676 3670 cascade="all, delete, delete-orphan")
3677 3671 versions = relationship('PullRequestVersion',
3678 3672 cascade="all, delete, delete-orphan",
3679 3673 lazy='dynamic')
3680 3674
3681 3675 @classmethod
3682 3676 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3683 3677 internal_methods=None):
3684 3678
3685 3679 class PullRequestDisplay(object):
3686 3680 """
3687 3681 Special object wrapper for showing PullRequest data via Versions
3688 3682 It mimics PR object as close as possible. This is read only object
3689 3683 just for display
3690 3684 """
3691 3685
3692 3686 def __init__(self, attrs, internal=None):
3693 3687 self.attrs = attrs
3694 3688 # internal have priority over the given ones via attrs
3695 3689 self.internal = internal or ['versions']
3696 3690
3697 3691 def __getattr__(self, item):
3698 3692 if item in self.internal:
3699 3693 return getattr(self, item)
3700 3694 try:
3701 3695 return self.attrs[item]
3702 3696 except KeyError:
3703 3697 raise AttributeError(
3704 3698 '%s object has no attribute %s' % (self, item))
3705 3699
3706 3700 def __repr__(self):
3707 3701 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3708 3702
3709 3703 def versions(self):
3710 3704 return pull_request_obj.versions.order_by(
3711 3705 PullRequestVersion.pull_request_version_id).all()
3712 3706
3713 3707 def is_closed(self):
3714 3708 return pull_request_obj.is_closed()
3715 3709
3716 3710 @property
3717 3711 def pull_request_version_id(self):
3718 3712 return getattr(pull_request_obj, 'pull_request_version_id', None)
3719 3713
3720 3714 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3721 3715
3722 3716 attrs.author = StrictAttributeDict(
3723 3717 pull_request_obj.author.get_api_data())
3724 3718 if pull_request_obj.target_repo:
3725 3719 attrs.target_repo = StrictAttributeDict(
3726 3720 pull_request_obj.target_repo.get_api_data())
3727 3721 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3728 3722
3729 3723 if pull_request_obj.source_repo:
3730 3724 attrs.source_repo = StrictAttributeDict(
3731 3725 pull_request_obj.source_repo.get_api_data())
3732 3726 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3733 3727
3734 3728 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3735 3729 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3736 3730 attrs.revisions = pull_request_obj.revisions
3737 3731
3738 3732 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3739 3733 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3740 3734 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3741 3735
3742 3736 return PullRequestDisplay(attrs, internal=internal_methods)
3743 3737
3744 3738 def is_closed(self):
3745 3739 return self.status == self.STATUS_CLOSED
3746 3740
3747 3741 def __json__(self):
3748 3742 return {
3749 3743 'revisions': self.revisions,
3750 3744 }
3751 3745
3752 3746 def calculated_review_status(self):
3753 3747 from rhodecode.model.changeset_status import ChangesetStatusModel
3754 3748 return ChangesetStatusModel().calculated_review_status(self)
3755 3749
3756 3750 def reviewers_statuses(self):
3757 3751 from rhodecode.model.changeset_status import ChangesetStatusModel
3758 3752 return ChangesetStatusModel().reviewers_statuses(self)
3759 3753
3760 3754 @property
3761 3755 def workspace_id(self):
3762 3756 from rhodecode.model.pull_request import PullRequestModel
3763 3757 return PullRequestModel()._workspace_id(self)
3764 3758
3765 3759 def get_shadow_repo(self):
3766 3760 workspace_id = self.workspace_id
3767 3761 vcs_obj = self.target_repo.scm_instance()
3768 3762 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3769 3763 self.target_repo.repo_id, workspace_id)
3770 3764 if os.path.isdir(shadow_repository_path):
3771 3765 return vcs_obj._get_shadow_instance(shadow_repository_path)
3772 3766
3773 3767
3774 3768 class PullRequestVersion(Base, _PullRequestBase):
3775 3769 __tablename__ = 'pull_request_versions'
3776 3770 __table_args__ = (
3777 3771 base_table_args,
3778 3772 )
3779 3773
3780 3774 pull_request_version_id = Column(
3781 3775 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3782 3776 pull_request_id = Column(
3783 3777 'pull_request_id', Integer(),
3784 3778 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3785 3779 pull_request = relationship('PullRequest')
3786 3780
3787 3781 def __repr__(self):
3788 3782 if self.pull_request_version_id:
3789 3783 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3790 3784 else:
3791 3785 return '<DB:PullRequestVersion at %#x>' % id(self)
3792 3786
3793 3787 @property
3794 3788 def reviewers(self):
3795 3789 return self.pull_request.reviewers
3796 3790
3797 3791 @property
3798 3792 def versions(self):
3799 3793 return self.pull_request.versions
3800 3794
3801 3795 def is_closed(self):
3802 3796 # calculate from original
3803 3797 return self.pull_request.status == self.STATUS_CLOSED
3804 3798
3805 3799 def calculated_review_status(self):
3806 3800 return self.pull_request.calculated_review_status()
3807 3801
3808 3802 def reviewers_statuses(self):
3809 3803 return self.pull_request.reviewers_statuses()
3810 3804
3811 3805
3812 3806 class PullRequestReviewers(Base, BaseModel):
3813 3807 __tablename__ = 'pull_request_reviewers'
3814 3808 __table_args__ = (
3815 3809 base_table_args,
3816 3810 )
3817 3811
3818 3812 @hybrid_property
3819 3813 def reasons(self):
3820 3814 if not self._reasons:
3821 3815 return []
3822 3816 return self._reasons
3823 3817
3824 3818 @reasons.setter
3825 3819 def reasons(self, val):
3826 3820 val = val or []
3827 3821 if any(not isinstance(x, basestring) for x in val):
3828 3822 raise Exception('invalid reasons type, must be list of strings')
3829 3823 self._reasons = val
3830 3824
3831 3825 pull_requests_reviewers_id = Column(
3832 3826 'pull_requests_reviewers_id', Integer(), nullable=False,
3833 3827 primary_key=True)
3834 3828 pull_request_id = Column(
3835 3829 "pull_request_id", Integer(),
3836 3830 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3837 3831 user_id = Column(
3838 3832 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3839 3833 _reasons = Column(
3840 3834 'reason', MutationList.as_mutable(
3841 3835 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3842 3836
3843 3837 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3844 3838 user = relationship('User')
3845 3839 pull_request = relationship('PullRequest')
3846 3840
3847 3841 rule_data = Column(
3848 3842 'rule_data_json',
3849 3843 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
3850 3844
3851 3845 def rule_user_group_data(self):
3852 3846 """
3853 3847 Returns the voting user group rule data for this reviewer
3854 3848 """
3855 3849
3856 3850 if self.rule_data and 'vote_rule' in self.rule_data:
3857 3851 user_group_data = {}
3858 3852 if 'rule_user_group_entry_id' in self.rule_data:
3859 3853 # means a group with voting rules !
3860 3854 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
3861 3855 user_group_data['name'] = self.rule_data['rule_name']
3862 3856 user_group_data['vote_rule'] = self.rule_data['vote_rule']
3863 3857
3864 3858 return user_group_data
3865 3859
3866 3860 def __unicode__(self):
3867 3861 return u"<%s('id:%s')>" % (self.__class__.__name__,
3868 3862 self.pull_requests_reviewers_id)
3869 3863
3870 3864
3871 3865 class Notification(Base, BaseModel):
3872 3866 __tablename__ = 'notifications'
3873 3867 __table_args__ = (
3874 3868 Index('notification_type_idx', 'type'),
3875 3869 base_table_args,
3876 3870 )
3877 3871
3878 3872 TYPE_CHANGESET_COMMENT = u'cs_comment'
3879 3873 TYPE_MESSAGE = u'message'
3880 3874 TYPE_MENTION = u'mention'
3881 3875 TYPE_REGISTRATION = u'registration'
3882 3876 TYPE_PULL_REQUEST = u'pull_request'
3883 3877 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3884 3878
3885 3879 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3886 3880 subject = Column('subject', Unicode(512), nullable=True)
3887 3881 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3888 3882 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3889 3883 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3890 3884 type_ = Column('type', Unicode(255))
3891 3885
3892 3886 created_by_user = relationship('User')
3893 3887 notifications_to_users = relationship('UserNotification', lazy='joined',
3894 3888 cascade="all, delete, delete-orphan")
3895 3889
3896 3890 @property
3897 3891 def recipients(self):
3898 3892 return [x.user for x in UserNotification.query()\
3899 3893 .filter(UserNotification.notification == self)\
3900 3894 .order_by(UserNotification.user_id.asc()).all()]
3901 3895
3902 3896 @classmethod
3903 3897 def create(cls, created_by, subject, body, recipients, type_=None):
3904 3898 if type_ is None:
3905 3899 type_ = Notification.TYPE_MESSAGE
3906 3900
3907 3901 notification = cls()
3908 3902 notification.created_by_user = created_by
3909 3903 notification.subject = subject
3910 3904 notification.body = body
3911 3905 notification.type_ = type_
3912 3906 notification.created_on = datetime.datetime.now()
3913 3907
3914 3908 # For each recipient link the created notification to his account
3915 3909 for u in recipients:
3916 3910 assoc = UserNotification()
3917 3911 assoc.user_id = u.user_id
3918 3912 assoc.notification = notification
3919 3913
3920 3914 # if created_by is inside recipients mark his notification
3921 3915 # as read
3922 3916 if u.user_id == created_by.user_id:
3923 3917 assoc.read = True
3924 3918 Session().add(assoc)
3925 3919
3926 3920 Session().add(notification)
3927 3921
3928 3922 return notification
3929 3923
3930 3924
3931 3925 class UserNotification(Base, BaseModel):
3932 3926 __tablename__ = 'user_to_notification'
3933 3927 __table_args__ = (
3934 3928 UniqueConstraint('user_id', 'notification_id'),
3935 3929 base_table_args
3936 3930 )
3937 3931
3938 3932 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3939 3933 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3940 3934 read = Column('read', Boolean, default=False)
3941 3935 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3942 3936
3943 3937 user = relationship('User', lazy="joined")
3944 3938 notification = relationship('Notification', lazy="joined",
3945 3939 order_by=lambda: Notification.created_on.desc(),)
3946 3940
3947 3941 def mark_as_read(self):
3948 3942 self.read = True
3949 3943 Session().add(self)
3950 3944
3951 3945
3952 3946 class Gist(Base, BaseModel):
3953 3947 __tablename__ = 'gists'
3954 3948 __table_args__ = (
3955 3949 Index('g_gist_access_id_idx', 'gist_access_id'),
3956 3950 Index('g_created_on_idx', 'created_on'),
3957 3951 base_table_args
3958 3952 )
3959 3953
3960 3954 GIST_PUBLIC = u'public'
3961 3955 GIST_PRIVATE = u'private'
3962 3956 DEFAULT_FILENAME = u'gistfile1.txt'
3963 3957
3964 3958 ACL_LEVEL_PUBLIC = u'acl_public'
3965 3959 ACL_LEVEL_PRIVATE = u'acl_private'
3966 3960
3967 3961 gist_id = Column('gist_id', Integer(), primary_key=True)
3968 3962 gist_access_id = Column('gist_access_id', Unicode(250))
3969 3963 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3970 3964 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3971 3965 gist_expires = Column('gist_expires', Float(53), nullable=False)
3972 3966 gist_type = Column('gist_type', Unicode(128), nullable=False)
3973 3967 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3974 3968 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3975 3969 acl_level = Column('acl_level', Unicode(128), nullable=True)
3976 3970
3977 3971 owner = relationship('User')
3978 3972
3979 3973 def __repr__(self):
3980 3974 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3981 3975
3982 3976 @hybrid_property
3983 3977 def description_safe(self):
3984 3978 from rhodecode.lib import helpers as h
3985 3979 return h.escape(self.gist_description)
3986 3980
3987 3981 @classmethod
3988 3982 def get_or_404(cls, id_):
3989 3983 from pyramid.httpexceptions import HTTPNotFound
3990 3984
3991 3985 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3992 3986 if not res:
3993 3987 raise HTTPNotFound()
3994 3988 return res
3995 3989
3996 3990 @classmethod
3997 3991 def get_by_access_id(cls, gist_access_id):
3998 3992 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3999 3993
4000 3994 def gist_url(self):
4001 3995 from rhodecode.model.gist import GistModel
4002 3996 return GistModel().get_url(self)
4003 3997
4004 3998 @classmethod
4005 3999 def base_path(cls):
4006 4000 """
4007 4001 Returns base path when all gists are stored
4008 4002
4009 4003 :param cls:
4010 4004 """
4011 4005 from rhodecode.model.gist import GIST_STORE_LOC
4012 4006 q = Session().query(RhodeCodeUi)\
4013 4007 .filter(RhodeCodeUi.ui_key == URL_SEP)
4014 4008 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4015 4009 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4016 4010
4017 4011 def get_api_data(self):
4018 4012 """
4019 4013 Common function for generating gist related data for API
4020 4014 """
4021 4015 gist = self
4022 4016 data = {
4023 4017 'gist_id': gist.gist_id,
4024 4018 'type': gist.gist_type,
4025 4019 'access_id': gist.gist_access_id,
4026 4020 'description': gist.gist_description,
4027 4021 'url': gist.gist_url(),
4028 4022 'expires': gist.gist_expires,
4029 4023 'created_on': gist.created_on,
4030 4024 'modified_at': gist.modified_at,
4031 4025 'content': None,
4032 4026 'acl_level': gist.acl_level,
4033 4027 }
4034 4028 return data
4035 4029
4036 4030 def __json__(self):
4037 4031 data = dict(
4038 4032 )
4039 4033 data.update(self.get_api_data())
4040 4034 return data
4041 4035 # SCM functions
4042 4036
4043 4037 def scm_instance(self, **kwargs):
4044 4038 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4045 4039 return get_vcs_instance(
4046 4040 repo_path=safe_str(full_repo_path), create=False)
4047 4041
4048 4042
4049 4043 class ExternalIdentity(Base, BaseModel):
4050 4044 __tablename__ = 'external_identities'
4051 4045 __table_args__ = (
4052 4046 Index('local_user_id_idx', 'local_user_id'),
4053 4047 Index('external_id_idx', 'external_id'),
4054 4048 base_table_args
4055 4049 )
4056 4050
4057 4051 external_id = Column('external_id', Unicode(255), default=u'',
4058 4052 primary_key=True)
4059 4053 external_username = Column('external_username', Unicode(1024), default=u'')
4060 4054 local_user_id = Column('local_user_id', Integer(),
4061 4055 ForeignKey('users.user_id'), primary_key=True)
4062 4056 provider_name = Column('provider_name', Unicode(255), default=u'',
4063 4057 primary_key=True)
4064 4058 access_token = Column('access_token', String(1024), default=u'')
4065 4059 alt_token = Column('alt_token', String(1024), default=u'')
4066 4060 token_secret = Column('token_secret', String(1024), default=u'')
4067 4061
4068 4062 @classmethod
4069 4063 def by_external_id_and_provider(cls, external_id, provider_name,
4070 4064 local_user_id=None):
4071 4065 """
4072 4066 Returns ExternalIdentity instance based on search params
4073 4067
4074 4068 :param external_id:
4075 4069 :param provider_name:
4076 4070 :return: ExternalIdentity
4077 4071 """
4078 4072 query = cls.query()
4079 4073 query = query.filter(cls.external_id == external_id)
4080 4074 query = query.filter(cls.provider_name == provider_name)
4081 4075 if local_user_id:
4082 4076 query = query.filter(cls.local_user_id == local_user_id)
4083 4077 return query.first()
4084 4078
4085 4079 @classmethod
4086 4080 def user_by_external_id_and_provider(cls, external_id, provider_name):
4087 4081 """
4088 4082 Returns User instance based on search params
4089 4083
4090 4084 :param external_id:
4091 4085 :param provider_name:
4092 4086 :return: User
4093 4087 """
4094 4088 query = User.query()
4095 4089 query = query.filter(cls.external_id == external_id)
4096 4090 query = query.filter(cls.provider_name == provider_name)
4097 4091 query = query.filter(User.user_id == cls.local_user_id)
4098 4092 return query.first()
4099 4093
4100 4094 @classmethod
4101 4095 def by_local_user_id(cls, local_user_id):
4102 4096 """
4103 4097 Returns all tokens for user
4104 4098
4105 4099 :param local_user_id:
4106 4100 :return: ExternalIdentity
4107 4101 """
4108 4102 query = cls.query()
4109 4103 query = query.filter(cls.local_user_id == local_user_id)
4110 4104 return query
4111 4105
4112 4106
4113 4107 class Integration(Base, BaseModel):
4114 4108 __tablename__ = 'integrations'
4115 4109 __table_args__ = (
4116 4110 base_table_args
4117 4111 )
4118 4112
4119 4113 integration_id = Column('integration_id', Integer(), primary_key=True)
4120 4114 integration_type = Column('integration_type', String(255))
4121 4115 enabled = Column('enabled', Boolean(), nullable=False)
4122 4116 name = Column('name', String(255), nullable=False)
4123 4117 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4124 4118 default=False)
4125 4119
4126 4120 settings = Column(
4127 4121 'settings_json', MutationObj.as_mutable(
4128 4122 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4129 4123 repo_id = Column(
4130 4124 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4131 4125 nullable=True, unique=None, default=None)
4132 4126 repo = relationship('Repository', lazy='joined')
4133 4127
4134 4128 repo_group_id = Column(
4135 4129 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4136 4130 nullable=True, unique=None, default=None)
4137 4131 repo_group = relationship('RepoGroup', lazy='joined')
4138 4132
4139 4133 @property
4140 4134 def scope(self):
4141 4135 if self.repo:
4142 4136 return repr(self.repo)
4143 4137 if self.repo_group:
4144 4138 if self.child_repos_only:
4145 4139 return repr(self.repo_group) + ' (child repos only)'
4146 4140 else:
4147 4141 return repr(self.repo_group) + ' (recursive)'
4148 4142 if self.child_repos_only:
4149 4143 return 'root_repos'
4150 4144 return 'global'
4151 4145
4152 4146 def __repr__(self):
4153 4147 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4154 4148
4155 4149
4156 4150 class RepoReviewRuleUser(Base, BaseModel):
4157 4151 __tablename__ = 'repo_review_rules_users'
4158 4152 __table_args__ = (
4159 4153 base_table_args
4160 4154 )
4161 4155
4162 4156 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4163 4157 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4164 4158 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4165 4159 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4166 4160 user = relationship('User')
4167 4161
4168 4162 def rule_data(self):
4169 4163 return {
4170 4164 'mandatory': self.mandatory
4171 4165 }
4172 4166
4173 4167
4174 4168 class RepoReviewRuleUserGroup(Base, BaseModel):
4175 4169 __tablename__ = 'repo_review_rules_users_groups'
4176 4170 __table_args__ = (
4177 4171 base_table_args
4178 4172 )
4179 4173
4180 4174 VOTE_RULE_ALL = -1
4181 4175
4182 4176 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4183 4177 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4184 4178 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4185 4179 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4186 4180 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4187 4181 users_group = relationship('UserGroup')
4188 4182
4189 4183 def rule_data(self):
4190 4184 return {
4191 4185 'mandatory': self.mandatory,
4192 4186 'vote_rule': self.vote_rule
4193 4187 }
4194 4188
4195 4189 @property
4196 4190 def vote_rule_label(self):
4197 4191 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4198 4192 return 'all must vote'
4199 4193 else:
4200 4194 return 'min. vote {}'.format(self.vote_rule)
4201 4195
4202 4196
4203 4197 class RepoReviewRule(Base, BaseModel):
4204 4198 __tablename__ = 'repo_review_rules'
4205 4199 __table_args__ = (
4206 4200 base_table_args
4207 4201 )
4208 4202
4209 4203 repo_review_rule_id = Column(
4210 4204 'repo_review_rule_id', Integer(), primary_key=True)
4211 4205 repo_id = Column(
4212 4206 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4213 4207 repo = relationship('Repository', backref='review_rules')
4214 4208
4215 4209 review_rule_name = Column('review_rule_name', String(255))
4216 4210 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4217 4211 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4218 4212 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4219 4213
4220 4214 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4221 4215 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4222 4216 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4223 4217 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4224 4218
4225 4219 rule_users = relationship('RepoReviewRuleUser')
4226 4220 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4227 4221
4228 4222 def _validate_pattern(self, value):
4229 4223 re.compile('^' + glob2re(value) + '$')
4230 4224
4231 4225 @hybrid_property
4232 4226 def source_branch_pattern(self):
4233 4227 return self._branch_pattern or '*'
4234 4228
4235 4229 @source_branch_pattern.setter
4236 4230 def source_branch_pattern(self, value):
4237 4231 self._validate_pattern(value)
4238 4232 self._branch_pattern = value or '*'
4239 4233
4240 4234 @hybrid_property
4241 4235 def target_branch_pattern(self):
4242 4236 return self._target_branch_pattern or '*'
4243 4237
4244 4238 @target_branch_pattern.setter
4245 4239 def target_branch_pattern(self, value):
4246 4240 self._validate_pattern(value)
4247 4241 self._target_branch_pattern = value or '*'
4248 4242
4249 4243 @hybrid_property
4250 4244 def file_pattern(self):
4251 4245 return self._file_pattern or '*'
4252 4246
4253 4247 @file_pattern.setter
4254 4248 def file_pattern(self, value):
4255 4249 self._validate_pattern(value)
4256 4250 self._file_pattern = value or '*'
4257 4251
4258 4252 def matches(self, source_branch, target_branch, files_changed):
4259 4253 """
4260 4254 Check if this review rule matches a branch/files in a pull request
4261 4255
4262 4256 :param source_branch: source branch name for the commit
4263 4257 :param target_branch: target branch name for the commit
4264 4258 :param files_changed: list of file paths changed in the pull request
4265 4259 """
4266 4260
4267 4261 source_branch = source_branch or ''
4268 4262 target_branch = target_branch or ''
4269 4263 files_changed = files_changed or []
4270 4264
4271 4265 branch_matches = True
4272 4266 if source_branch or target_branch:
4273 4267 if self.source_branch_pattern == '*':
4274 4268 source_branch_match = True
4275 4269 else:
4276 4270 if self.source_branch_pattern.startswith('re:'):
4277 4271 source_pattern = self.source_branch_pattern[3:]
4278 4272 else:
4279 4273 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4280 4274 source_branch_regex = re.compile(source_pattern)
4281 4275 source_branch_match = bool(source_branch_regex.search(source_branch))
4282 4276 if self.target_branch_pattern == '*':
4283 4277 target_branch_match = True
4284 4278 else:
4285 4279 if self.target_branch_pattern.startswith('re:'):
4286 4280 target_pattern = self.target_branch_pattern[3:]
4287 4281 else:
4288 4282 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4289 4283 target_branch_regex = re.compile(target_pattern)
4290 4284 target_branch_match = bool(target_branch_regex.search(target_branch))
4291 4285
4292 4286 branch_matches = source_branch_match and target_branch_match
4293 4287
4294 4288 files_matches = True
4295 4289 if self.file_pattern != '*':
4296 4290 files_matches = False
4297 4291 if self.file_pattern.startswith('re:'):
4298 4292 file_pattern = self.file_pattern[3:]
4299 4293 else:
4300 4294 file_pattern = glob2re(self.file_pattern)
4301 4295 file_regex = re.compile(file_pattern)
4302 4296 for filename in files_changed:
4303 4297 if file_regex.search(filename):
4304 4298 files_matches = True
4305 4299 break
4306 4300
4307 4301 return branch_matches and files_matches
4308 4302
4309 4303 @property
4310 4304 def review_users(self):
4311 4305 """ Returns the users which this rule applies to """
4312 4306
4313 4307 users = collections.OrderedDict()
4314 4308
4315 4309 for rule_user in self.rule_users:
4316 4310 if rule_user.user.active:
4317 4311 if rule_user.user not in users:
4318 4312 users[rule_user.user.username] = {
4319 4313 'user': rule_user.user,
4320 4314 'source': 'user',
4321 4315 'source_data': {},
4322 4316 'data': rule_user.rule_data()
4323 4317 }
4324 4318
4325 4319 for rule_user_group in self.rule_user_groups:
4326 4320 source_data = {
4327 4321 'user_group_id': rule_user_group.users_group.users_group_id,
4328 4322 'name': rule_user_group.users_group.users_group_name,
4329 4323 'members': len(rule_user_group.users_group.members)
4330 4324 }
4331 4325 for member in rule_user_group.users_group.members:
4332 4326 if member.user.active:
4333 4327 key = member.user.username
4334 4328 if key in users:
4335 4329 # skip this member as we have him already
4336 4330 # this prevents from override the "first" matched
4337 4331 # users with duplicates in multiple groups
4338 4332 continue
4339 4333
4340 4334 users[key] = {
4341 4335 'user': member.user,
4342 4336 'source': 'user_group',
4343 4337 'source_data': source_data,
4344 4338 'data': rule_user_group.rule_data()
4345 4339 }
4346 4340
4347 4341 return users
4348 4342
4349 4343 def user_group_vote_rule(self):
4350 4344 rules = []
4351 4345 if self.rule_user_groups:
4352 4346 for user_group in self.rule_user_groups:
4353 4347 rules.append(user_group)
4354 4348 return rules
4355 4349
4356 4350 def __repr__(self):
4357 4351 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4358 4352 self.repo_review_rule_id, self.repo)
4359 4353
4360 4354
4361 4355 class ScheduleEntry(Base, BaseModel):
4362 4356 __tablename__ = 'schedule_entries'
4363 4357 __table_args__ = (
4364 4358 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4365 4359 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4366 4360 base_table_args,
4367 4361 )
4368 4362
4369 4363 schedule_types = ['crontab', 'timedelta', 'integer']
4370 4364 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4371 4365
4372 4366 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4373 4367 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4374 4368 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4375 4369
4376 4370 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4377 4371 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4378 4372
4379 4373 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4380 4374 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4381 4375
4382 4376 # task
4383 4377 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4384 4378 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4385 4379 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4386 4380 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4387 4381
4388 4382 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4389 4383 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4390 4384
4391 4385 @hybrid_property
4392 4386 def schedule_type(self):
4393 4387 return self._schedule_type
4394 4388
4395 4389 @schedule_type.setter
4396 4390 def schedule_type(self, val):
4397 4391 if val not in self.schedule_types:
4398 4392 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4399 4393 val, self.schedule_type))
4400 4394
4401 4395 self._schedule_type = val
4402 4396
4403 4397 @classmethod
4404 4398 def get_uid(cls, obj):
4405 4399 args = obj.task_args
4406 4400 kwargs = obj.task_kwargs
4407 4401 if isinstance(args, JsonRaw):
4408 4402 try:
4409 4403 args = json.loads(args)
4410 4404 except ValueError:
4411 4405 args = tuple()
4412 4406
4413 4407 if isinstance(kwargs, JsonRaw):
4414 4408 try:
4415 4409 kwargs = json.loads(kwargs)
4416 4410 except ValueError:
4417 4411 kwargs = dict()
4418 4412
4419 4413 dot_notation = obj.task_dot_notation
4420 4414 val = '.'.join(map(safe_str, [
4421 4415 sorted(dot_notation), args, sorted(kwargs.items())]))
4422 4416 return hashlib.sha1(val).hexdigest()
4423 4417
4424 4418 @classmethod
4425 4419 def get_by_schedule_name(cls, schedule_name):
4426 4420 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
4427 4421
4428 4422 @classmethod
4429 4423 def get_by_schedule_id(cls, schedule_id):
4430 4424 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
4431 4425
4432 4426 @property
4433 4427 def task(self):
4434 4428 return self.task_dot_notation
4435 4429
4436 4430 @property
4437 4431 def schedule(self):
4438 4432 from rhodecode.lib.celerylib.utils import raw_2_schedule
4439 4433 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
4440 4434 return schedule
4441 4435
4442 4436 @property
4443 4437 def args(self):
4444 4438 try:
4445 4439 return list(self.task_args or [])
4446 4440 except ValueError:
4447 4441 return list()
4448 4442
4449 4443 @property
4450 4444 def kwargs(self):
4451 4445 try:
4452 4446 return dict(self.task_kwargs or {})
4453 4447 except ValueError:
4454 4448 return dict()
4455 4449
4456 4450 def _as_raw(self, val):
4457 4451 if hasattr(val, 'de_coerce'):
4458 4452 val = val.de_coerce()
4459 4453 if val:
4460 4454 val = json.dumps(val)
4461 4455
4462 4456 return val
4463 4457
4464 4458 @property
4465 4459 def schedule_definition_raw(self):
4466 4460 return self._as_raw(self.schedule_definition)
4467 4461
4468 4462 @property
4469 4463 def args_raw(self):
4470 4464 return self._as_raw(self.task_args)
4471 4465
4472 4466 @property
4473 4467 def kwargs_raw(self):
4474 4468 return self._as_raw(self.task_kwargs)
4475 4469
4476 4470 def __repr__(self):
4477 4471 return '<DB:ScheduleEntry({}:{})>'.format(
4478 4472 self.schedule_entry_id, self.schedule_name)
4479 4473
4480 4474
4481 4475 @event.listens_for(ScheduleEntry, 'before_update')
4482 4476 def update_task_uid(mapper, connection, target):
4483 4477 target.task_uid = ScheduleEntry.get_uid(target)
4484 4478
4485 4479
4486 4480 @event.listens_for(ScheduleEntry, 'before_insert')
4487 4481 def set_task_uid(mapper, connection, target):
4488 4482 target.task_uid = ScheduleEntry.get_uid(target)
4489 4483
4490 4484
4491 4485 class DbMigrateVersion(Base, BaseModel):
4492 4486 __tablename__ = 'db_migrate_version'
4493 4487 __table_args__ = (
4494 4488 base_table_args,
4495 4489 )
4496 4490
4497 4491 repository_id = Column('repository_id', String(250), primary_key=True)
4498 4492 repository_path = Column('repository_path', Text)
4499 4493 version = Column('version', Integer)
4500 4494
4501 4495 @classmethod
4502 4496 def set_version(cls, version):
4503 4497 """
4504 4498 Helper for forcing a different version, usually for debugging purposes via ishell.
4505 4499 """
4506 4500 ver = DbMigrateVersion.query().first()
4507 4501 ver.version = version
4508 4502 Session().commit()
4509 4503
4510 4504
4511 4505 class DbSession(Base, BaseModel):
4512 4506 __tablename__ = 'db_session'
4513 4507 __table_args__ = (
4514 4508 base_table_args,
4515 4509 )
4516 4510
4517 4511 def __repr__(self):
4518 4512 return '<DB:DbSession({})>'.format(self.id)
4519 4513
4520 4514 id = Column('id', Integer())
4521 4515 namespace = Column('namespace', String(255), primary_key=True)
4522 4516 accessed = Column('accessed', DateTime, nullable=False)
4523 4517 created = Column('created', DateTime, nullable=False)
4524 4518 data = Column('data', PickleType, nullable=False)
4525 4519
4526 4520
4527 4521 class BeakerCache(Base, BaseModel):
4528 4522 __tablename__ = 'beaker_cache'
4529 4523 __table_args__ = (
4530 4524 base_table_args,
4531 4525 )
4532 4526
4533 4527 def __repr__(self):
4534 4528 return '<DB:DbSession({})>'.format(self.id)
4535 4529
4536 4530 id = Column('id', Integer())
4537 4531 namespace = Column('namespace', String(255), primary_key=True)
4538 4532 accessed = Column('accessed', DateTime, nullable=False)
4539 4533 created = Column('created', DateTime, nullable=False)
4540 4534 data = Column('data', PickleType, nullable=False)
@@ -1,328 +1,329 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 import io
21 21 import re
22 22 import datetime
23 23 import logging
24 24 import Queue
25 25 import subprocess32
26 26 import os
27 27
28 28
29 29 from dateutil.parser import parse
30 30 from pyramid.i18n import get_localizer
31 31 from pyramid.threadlocal import get_current_request
32 32 from pyramid.interfaces import IRoutesMapper
33 33 from pyramid.settings import asbool
34 34 from pyramid.path import AssetResolver
35 35 from threading import Thread
36 36
37 37 from rhodecode.translation import _ as tsf
38 38 from rhodecode.config.jsroutes import generate_jsroutes_content
39 39 from rhodecode.lib import auth
40 40 from rhodecode.lib.base import get_auth_user
41 41
42 42
43 43 import rhodecode
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 def add_renderer_globals(event):
50 50 from rhodecode.lib import helpers
51 51
52 52 # TODO: When executed in pyramid view context the request is not available
53 53 # in the event. Find a better solution to get the request.
54 54 request = event['request'] or get_current_request()
55 55
56 56 # Add Pyramid translation as '_' to context
57 57 event['_'] = request.translate
58 58 event['_ungettext'] = request.plularize
59 59 event['h'] = helpers
60 60
61 61
62 62 def add_localizer(event):
63 63 request = event.request
64 64 localizer = request.localizer
65 65
66 66 def auto_translate(*args, **kwargs):
67 67 return localizer.translate(tsf(*args, **kwargs))
68 68
69 69 request.translate = auto_translate
70 70 request.plularize = localizer.pluralize
71 71
72 72
73 73 def set_user_lang(event):
74 74 request = event.request
75 75 cur_user = getattr(request, 'user', None)
76 76
77 77 if cur_user:
78 78 user_lang = cur_user.get_instance().user_data.get('language')
79 79 if user_lang:
80 80 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
81 81 event.request._LOCALE_ = user_lang
82 82
83 83
84 84 def add_request_user_context(event):
85 85 """
86 86 Adds auth user into request context
87 87 """
88 88 request = event.request
89 89 # access req_id as soon as possible
90 90 req_id = request.req_id
91 91
92 92 if hasattr(request, 'vcs_call'):
93 93 # skip vcs calls
94 94 return
95 95
96 96 if hasattr(request, 'rpc_method'):
97 97 # skip api calls
98 98 return
99 99
100 100 auth_user = get_auth_user(request)
101 101 request.user = auth_user
102 102 request.environ['rc_auth_user'] = auth_user
103 request.environ['rc_auth_user_id'] = auth_user.user_id
103 104 request.environ['rc_req_id'] = req_id
104 105
105 106
106 107 def inject_app_settings(event):
107 108 settings = event.app.registry.settings
108 109 # inject info about available permissions
109 110 auth.set_available_permissions(settings)
110 111
111 112
112 113 def scan_repositories_if_enabled(event):
113 114 """
114 115 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
115 116 does a repository scan if enabled in the settings.
116 117 """
117 118 settings = event.app.registry.settings
118 119 vcs_server_enabled = settings['vcs.server.enable']
119 120 import_on_startup = settings['startup.import_repos']
120 121 if vcs_server_enabled and import_on_startup:
121 122 from rhodecode.model.scm import ScmModel
122 123 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
123 124 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
124 125 repo2db_mapper(repositories, remove_obsolete=False)
125 126
126 127
127 128 def write_metadata_if_needed(event):
128 129 """
129 130 Writes upgrade metadata
130 131 """
131 132 import rhodecode
132 133 from rhodecode.lib import system_info
133 134 from rhodecode.lib import ext_json
134 135
135 136 fname = '.rcmetadata.json'
136 137 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
137 138 metadata_destination = os.path.join(ini_loc, fname)
138 139
139 140 def get_update_age():
140 141 now = datetime.datetime.utcnow()
141 142
142 143 with open(metadata_destination, 'rb') as f:
143 144 data = ext_json.json.loads(f.read())
144 145 if 'created_on' in data:
145 146 update_date = parse(data['created_on'])
146 147 diff = now - update_date
147 148 return diff.total_seconds() / 60.0
148 149
149 150 return 0
150 151
151 152 def write():
152 153 configuration = system_info.SysInfo(
153 154 system_info.rhodecode_config)()['value']
154 155 license_token = configuration['config']['license_token']
155 156
156 157 setup = dict(
157 158 workers=configuration['config']['server:main'].get(
158 159 'workers', '?'),
159 160 worker_type=configuration['config']['server:main'].get(
160 161 'worker_class', 'sync'),
161 162 )
162 163 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
163 164 del dbinfo['url']
164 165
165 166 metadata = dict(
166 167 desc='upgrade metadata info',
167 168 license_token=license_token,
168 169 created_on=datetime.datetime.utcnow().isoformat(),
169 170 usage=system_info.SysInfo(system_info.usage_info)()['value'],
170 171 platform=system_info.SysInfo(system_info.platform_type)()['value'],
171 172 database=dbinfo,
172 173 cpu=system_info.SysInfo(system_info.cpu)()['value'],
173 174 memory=system_info.SysInfo(system_info.memory)()['value'],
174 175 setup=setup
175 176 )
176 177
177 178 with open(metadata_destination, 'wb') as f:
178 179 f.write(ext_json.json.dumps(metadata))
179 180
180 181 settings = event.app.registry.settings
181 182 if settings.get('metadata.skip'):
182 183 return
183 184
184 185 # only write this every 24h, workers restart caused unwanted delays
185 186 try:
186 187 age_in_min = get_update_age()
187 188 except Exception:
188 189 age_in_min = 0
189 190
190 191 if age_in_min > 60 * 60 * 24:
191 192 return
192 193
193 194 try:
194 195 write()
195 196 except Exception:
196 197 pass
197 198
198 199
199 200 def write_js_routes_if_enabled(event):
200 201 registry = event.app.registry
201 202
202 203 mapper = registry.queryUtility(IRoutesMapper)
203 204 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
204 205
205 206 def _extract_route_information(route):
206 207 """
207 208 Convert a route into tuple(name, path, args), eg:
208 209 ('show_user', '/profile/%(username)s', ['username'])
209 210 """
210 211
211 212 routepath = route.pattern
212 213 pattern = route.pattern
213 214
214 215 def replace(matchobj):
215 216 if matchobj.group(1):
216 217 return "%%(%s)s" % matchobj.group(1).split(':')[0]
217 218 else:
218 219 return "%%(%s)s" % matchobj.group(2)
219 220
220 221 routepath = _argument_prog.sub(replace, routepath)
221 222
222 223 if not routepath.startswith('/'):
223 224 routepath = '/'+routepath
224 225
225 226 return (
226 227 route.name,
227 228 routepath,
228 229 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
229 230 for arg in _argument_prog.findall(pattern)]
230 231 )
231 232
232 233 def get_routes():
233 234 # pyramid routes
234 235 for route in mapper.get_routes():
235 236 if not route.name.startswith('__'):
236 237 yield _extract_route_information(route)
237 238
238 239 if asbool(registry.settings.get('generate_js_files', 'false')):
239 240 static_path = AssetResolver().resolve('rhodecode:public').abspath()
240 241 jsroutes = get_routes()
241 242 jsroutes_file_content = generate_jsroutes_content(jsroutes)
242 243 jsroutes_file_path = os.path.join(
243 244 static_path, 'js', 'rhodecode', 'routes.js')
244 245
245 246 try:
246 247 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
247 248 f.write(jsroutes_file_content)
248 249 except Exception:
249 250 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
250 251
251 252
252 253 class Subscriber(object):
253 254 """
254 255 Base class for subscribers to the pyramid event system.
255 256 """
256 257 def __call__(self, event):
257 258 self.run(event)
258 259
259 260 def run(self, event):
260 261 raise NotImplementedError('Subclass has to implement this.')
261 262
262 263
263 264 class AsyncSubscriber(Subscriber):
264 265 """
265 266 Subscriber that handles the execution of events in a separate task to not
266 267 block the execution of the code which triggers the event. It puts the
267 268 received events into a queue from which the worker process takes them in
268 269 order.
269 270 """
270 271 def __init__(self):
271 272 self._stop = False
272 273 self._eventq = Queue.Queue()
273 274 self._worker = self.create_worker()
274 275 self._worker.start()
275 276
276 277 def __call__(self, event):
277 278 self._eventq.put(event)
278 279
279 280 def create_worker(self):
280 281 worker = Thread(target=self.do_work)
281 282 worker.daemon = True
282 283 return worker
283 284
284 285 def stop_worker(self):
285 286 self._stop = False
286 287 self._eventq.put(None)
287 288 self._worker.join()
288 289
289 290 def do_work(self):
290 291 while not self._stop:
291 292 event = self._eventq.get()
292 293 if event is not None:
293 294 self.run(event)
294 295
295 296
296 297 class AsyncSubprocessSubscriber(AsyncSubscriber):
297 298 """
298 299 Subscriber that uses the subprocess32 module to execute a command if an
299 300 event is received. Events are handled asynchronously.
300 301 """
301 302
302 303 def __init__(self, cmd, timeout=None):
303 304 super(AsyncSubprocessSubscriber, self).__init__()
304 305 self._cmd = cmd
305 306 self._timeout = timeout
306 307
307 308 def run(self, event):
308 309 cmd = self._cmd
309 310 timeout = self._timeout
310 311 log.debug('Executing command %s.', cmd)
311 312
312 313 try:
313 314 output = subprocess32.check_output(
314 315 cmd, timeout=timeout, stderr=subprocess32.STDOUT)
315 316 log.debug('Command finished %s', cmd)
316 317 if output:
317 318 log.debug('Command output: %s', output)
318 319 except subprocess32.TimeoutExpired as e:
319 320 log.exception('Timeout while executing command.')
320 321 if e.output:
321 322 log.error('Command output: %s', e.output)
322 323 except subprocess32.CalledProcessError as e:
323 324 log.exception('Error while executing command.')
324 325 if e.output:
325 326 log.error('Command output: %s', e.output)
326 327 except:
327 328 log.exception(
328 329 'Exception while executing command %s.', cmd)
General Comments 0
You need to be logged in to leave comments. Login now