##// END OF EJS Templates
auth-tokens: updated logic of authentication to a common shared user method.
marcink -
r1421:5088d9a7 default
parent child Browse files
Show More
@@ -1,536 +1,536 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 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.lib.auth import AuthUser
39 39 from rhodecode.lib.base import get_ip_addr
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 43 from rhodecode.model.db import User, UserApiKeys
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 48 DEFAULT_URL = '/_admin/apiv2'
49 49
50 50
51 51 def find_methods(jsonrpc_methods, pattern):
52 52 matches = OrderedDict()
53 53 if not isinstance(pattern, (list, tuple)):
54 54 pattern = [pattern]
55 55
56 56 for single_pattern in pattern:
57 57 for method_name, method in jsonrpc_methods.items():
58 58 if fnmatch.fnmatch(method_name, single_pattern):
59 59 matches[method_name] = method
60 60 return matches
61 61
62 62
63 63 class ExtJsonRenderer(object):
64 64 """
65 65 Custom renderer that mkaes use of our ext_json lib
66 66
67 67 """
68 68
69 69 def __init__(self, serializer=json.dumps, **kw):
70 70 """ Any keyword arguments will be passed to the ``serializer``
71 71 function."""
72 72 self.serializer = serializer
73 73 self.kw = kw
74 74
75 75 def __call__(self, info):
76 76 """ Returns a plain JSON-encoded string with content-type
77 77 ``application/json``. The content-type may be overridden by
78 78 setting ``request.response.content_type``."""
79 79
80 80 def _render(value, system):
81 81 request = system.get('request')
82 82 if request is not None:
83 83 response = request.response
84 84 ct = response.content_type
85 85 if ct == response.default_content_type:
86 86 response.content_type = 'application/json'
87 87
88 88 return self.serializer(value, **self.kw)
89 89
90 90 return _render
91 91
92 92
93 93 def jsonrpc_response(request, result):
94 94 rpc_id = getattr(request, 'rpc_id', None)
95 95 response = request.response
96 96
97 97 # store content_type before render is called
98 98 ct = response.content_type
99 99
100 100 ret_value = ''
101 101 if rpc_id:
102 102 ret_value = {
103 103 'id': rpc_id,
104 104 'result': result,
105 105 'error': None,
106 106 }
107 107
108 108 # fetch deprecation warnings, and store it inside results
109 109 deprecation = getattr(request, 'rpc_deprecation', None)
110 110 if deprecation:
111 111 ret_value['DEPRECATION_WARNING'] = deprecation
112 112
113 113 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
114 114 response.body = safe_str(raw_body, response.charset)
115 115
116 116 if ct == response.default_content_type:
117 117 response.content_type = 'application/json'
118 118
119 119 return response
120 120
121 121
122 122 def jsonrpc_error(request, message, retid=None, code=None):
123 123 """
124 124 Generate a Response object with a JSON-RPC error body
125 125
126 126 :param code:
127 127 :param retid:
128 128 :param message:
129 129 """
130 130 err_dict = {'id': retid, 'result': None, 'error': message}
131 131 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
132 132 return Response(
133 133 body=body,
134 134 status=code,
135 135 content_type='application/json'
136 136 )
137 137
138 138
139 139 def exception_view(exc, request):
140 140 rpc_id = getattr(request, 'rpc_id', None)
141 141
142 142 fault_message = 'undefined error'
143 143 if isinstance(exc, JSONRPCError):
144 144 fault_message = exc.message
145 145 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
146 146 elif isinstance(exc, JSONRPCValidationError):
147 147 colander_exc = exc.colander_exception
148 148 # TODO(marcink): think maybe of nicer way to serialize errors ?
149 149 fault_message = colander_exc.asdict()
150 150 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
151 151 elif isinstance(exc, JSONRPCForbidden):
152 152 fault_message = 'Access was denied to this resource.'
153 153 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
154 154 elif isinstance(exc, HTTPNotFound):
155 155 method = request.rpc_method
156 156 log.debug('json-rpc method `%s` not found in list of '
157 157 'api calls: %s, rpc_id:%s',
158 158 method, request.registry.jsonrpc_methods.keys(), rpc_id)
159 159
160 160 similar = 'none'
161 161 try:
162 162 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 163 similar_found = find_methods(
164 164 request.registry.jsonrpc_methods, similar_paterns)
165 165 similar = ', '.join(similar_found.keys()) or similar
166 166 except Exception:
167 167 # make the whole above block safe
168 168 pass
169 169
170 170 fault_message = "No such method: {}. Similar methods: {}".format(
171 171 method, similar)
172 172
173 173 return jsonrpc_error(request, fault_message, rpc_id)
174 174
175 175
176 176 def request_view(request):
177 177 """
178 178 Main request handling method. It handles all logic to call a specific
179 179 exposed method
180 180 """
181 181
182 182 # check if we can find this session using api_key, get_by_auth_token
183 183 # search not expired tokens only
184 184
185 185 try:
186 186 api_user = User.get_by_auth_token(request.rpc_api_key)
187 187
188 188 if api_user is None:
189 189 return jsonrpc_error(
190 190 request, retid=request.rpc_id, message='Invalid API KEY')
191 191
192 192 if not api_user.active:
193 193 return jsonrpc_error(
194 194 request, retid=request.rpc_id,
195 195 message='Request from this user not allowed')
196 196
197 197 # check if we are allowed to use this IP
198 198 auth_u = AuthUser(
199 199 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
200 200 if not auth_u.ip_allowed:
201 201 return jsonrpc_error(
202 202 request, retid=request.rpc_id,
203 203 message='Request from IP:%s not allowed' % (
204 204 request.rpc_ip_addr,))
205 205 else:
206 206 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
207 207
208 208 # register our auth-user
209 209 request.rpc_user = auth_u
210 210
211 211 # now check if token is valid for API
212 role = UserApiKeys.ROLE_API
213 extra_auth_tokens = [
214 x.api_key for x in User.extra_valid_auth_tokens(api_user, role=role)]
215 active_tokens = [api_user.api_key] + extra_auth_tokens
212 auth_token = request.rpc_api_key
213 token_match = api_user.authenticate_by_token(
214 auth_token, roles=[UserApiKeys.ROLE_API], include_builtin_token=True)
215 invalid_token = not token_match
216 216
217 log.debug('Checking if API key has proper role')
218 if request.rpc_api_key not in active_tokens:
217 log.debug('Checking if API KEY is valid with proper role')
218 if invalid_token:
219 219 return jsonrpc_error(
220 220 request, retid=request.rpc_id,
221 message='API KEY has bad role for an API call')
221 message='API KEY invalid or, has bad role for an API call')
222 222
223 except Exception as e:
223 except Exception:
224 224 log.exception('Error on API AUTH')
225 225 return jsonrpc_error(
226 226 request, retid=request.rpc_id, message='Invalid API KEY')
227 227
228 228 method = request.rpc_method
229 229 func = request.registry.jsonrpc_methods[method]
230 230
231 231 # now that we have a method, add request._req_params to
232 232 # self.kargs and dispatch control to WGIController
233 233 argspec = inspect.getargspec(func)
234 234 arglist = argspec[0]
235 235 defaults = map(type, argspec[3] or [])
236 236 default_empty = types.NotImplementedType
237 237
238 238 # kw arguments required by this method
239 239 func_kwargs = dict(itertools.izip_longest(
240 240 reversed(arglist), reversed(defaults), fillvalue=default_empty))
241 241
242 242 # This attribute will need to be first param of a method that uses
243 243 # api_key, which is translated to instance of user at that name
244 244 user_var = 'apiuser'
245 245 request_var = 'request'
246 246
247 247 for arg in [user_var, request_var]:
248 248 if arg not in arglist:
249 249 return jsonrpc_error(
250 250 request,
251 251 retid=request.rpc_id,
252 252 message='This method [%s] does not support '
253 253 'required parameter `%s`' % (func.__name__, arg))
254 254
255 255 # get our arglist and check if we provided them as args
256 256 for arg, default in func_kwargs.items():
257 257 if arg in [user_var, request_var]:
258 258 # user_var and request_var are pre-hardcoded parameters and we
259 259 # don't need to do any translation
260 260 continue
261 261
262 262 # skip the required param check if it's default value is
263 263 # NotImplementedType (default_empty)
264 264 if default == default_empty and arg not in request.rpc_params:
265 265 return jsonrpc_error(
266 266 request,
267 267 retid=request.rpc_id,
268 268 message=('Missing non optional `%s` arg in JSON DATA' % arg)
269 269 )
270 270
271 271 # sanitize extra passed arguments
272 272 for k in request.rpc_params.keys()[:]:
273 273 if k not in func_kwargs:
274 274 del request.rpc_params[k]
275 275
276 276 call_params = request.rpc_params
277 277 call_params.update({
278 278 'request': request,
279 279 'apiuser': auth_u
280 280 })
281 281 try:
282 282 ret_value = func(**call_params)
283 283 return jsonrpc_response(request, ret_value)
284 284 except JSONRPCBaseError:
285 285 raise
286 286 except Exception:
287 287 log.exception('Unhandled exception occurred on api call: %s', func)
288 288 return jsonrpc_error(request, retid=request.rpc_id,
289 289 message='Internal server error')
290 290
291 291
292 292 def setup_request(request):
293 293 """
294 294 Parse a JSON-RPC request body. It's used inside the predicates method
295 295 to validate and bootstrap requests for usage in rpc calls.
296 296
297 297 We need to raise JSONRPCError here if we want to return some errors back to
298 298 user.
299 299 """
300 300
301 301 log.debug('Executing setup request: %r', request)
302 302 request.rpc_ip_addr = get_ip_addr(request.environ)
303 303 # TODO(marcink): deprecate GET at some point
304 304 if request.method not in ['POST', 'GET']:
305 305 log.debug('unsupported request method "%s"', request.method)
306 306 raise JSONRPCError(
307 307 'unsupported request method "%s". Please use POST' % request.method)
308 308
309 309 if 'CONTENT_LENGTH' not in request.environ:
310 310 log.debug("No Content-Length")
311 311 raise JSONRPCError("Empty body, No Content-Length in request")
312 312
313 313 else:
314 314 length = request.environ['CONTENT_LENGTH']
315 315 log.debug('Content-Length: %s', length)
316 316
317 317 if length == 0:
318 318 log.debug("Content-Length is 0")
319 319 raise JSONRPCError("Content-Length is 0")
320 320
321 321 raw_body = request.body
322 322 try:
323 323 json_body = json.loads(raw_body)
324 324 except ValueError as e:
325 325 # catch JSON errors Here
326 326 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
327 327
328 328 request.rpc_id = json_body.get('id')
329 329 request.rpc_method = json_body.get('method')
330 330
331 331 # check required base parameters
332 332 try:
333 333 api_key = json_body.get('api_key')
334 334 if not api_key:
335 335 api_key = json_body.get('auth_token')
336 336
337 337 if not api_key:
338 338 raise KeyError('api_key or auth_token')
339 339
340 340 # TODO(marcink): support passing in token in request header
341 341
342 342 request.rpc_api_key = api_key
343 343 request.rpc_id = json_body['id']
344 344 request.rpc_method = json_body['method']
345 345 request.rpc_params = json_body['args'] \
346 346 if isinstance(json_body['args'], dict) else {}
347 347
348 348 log.debug(
349 349 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
350 350 except KeyError as e:
351 351 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
352 352
353 353 log.debug('setup complete, now handling method:%s rpcid:%s',
354 354 request.rpc_method, request.rpc_id, )
355 355
356 356
357 357 class RoutePredicate(object):
358 358 def __init__(self, val, config):
359 359 self.val = val
360 360
361 361 def text(self):
362 362 return 'jsonrpc route = %s' % self.val
363 363
364 364 phash = text
365 365
366 366 def __call__(self, info, request):
367 367 if self.val:
368 368 # potentially setup and bootstrap our call
369 369 setup_request(request)
370 370
371 371 # Always return True so that even if it isn't a valid RPC it
372 372 # will fall through to the underlaying handlers like notfound_view
373 373 return True
374 374
375 375
376 376 class NotFoundPredicate(object):
377 377 def __init__(self, val, config):
378 378 self.val = val
379 379 self.methods = config.registry.jsonrpc_methods
380 380
381 381 def text(self):
382 382 return 'jsonrpc method not found = {}.'.format(self.val)
383 383
384 384 phash = text
385 385
386 386 def __call__(self, info, request):
387 387 return hasattr(request, 'rpc_method')
388 388
389 389
390 390 class MethodPredicate(object):
391 391 def __init__(self, val, config):
392 392 self.method = val
393 393
394 394 def text(self):
395 395 return 'jsonrpc method = %s' % self.method
396 396
397 397 phash = text
398 398
399 399 def __call__(self, context, request):
400 400 # we need to explicitly return False here, so pyramid doesn't try to
401 401 # execute our view directly. We need our main handler to execute things
402 402 return getattr(request, 'rpc_method') == self.method
403 403
404 404
405 405 def add_jsonrpc_method(config, view, **kwargs):
406 406 # pop the method name
407 407 method = kwargs.pop('method', None)
408 408
409 409 if method is None:
410 410 raise ConfigurationError(
411 411 'Cannot register a JSON-RPC method without specifying the '
412 412 '"method"')
413 413
414 414 # we define custom predicate, to enable to detect conflicting methods,
415 415 # those predicates are kind of "translation" from the decorator variables
416 416 # to internal predicates names
417 417
418 418 kwargs['jsonrpc_method'] = method
419 419
420 420 # register our view into global view store for validation
421 421 config.registry.jsonrpc_methods[method] = view
422 422
423 423 # we're using our main request_view handler, here, so each method
424 424 # has a unified handler for itself
425 425 config.add_view(request_view, route_name='apiv2', **kwargs)
426 426
427 427
428 428 class jsonrpc_method(object):
429 429 """
430 430 decorator that works similar to @add_view_config decorator,
431 431 but tailored for our JSON RPC
432 432 """
433 433
434 434 venusian = venusian # for testing injection
435 435
436 436 def __init__(self, method=None, **kwargs):
437 437 self.method = method
438 438 self.kwargs = kwargs
439 439
440 440 def __call__(self, wrapped):
441 441 kwargs = self.kwargs.copy()
442 442 kwargs['method'] = self.method or wrapped.__name__
443 443 depth = kwargs.pop('_depth', 0)
444 444
445 445 def callback(context, name, ob):
446 446 config = context.config.with_package(info.module)
447 447 config.add_jsonrpc_method(view=ob, **kwargs)
448 448
449 449 info = venusian.attach(wrapped, callback, category='pyramid',
450 450 depth=depth + 1)
451 451 if info.scope == 'class':
452 452 # ensure that attr is set if decorating a class method
453 453 kwargs.setdefault('attr', wrapped.__name__)
454 454
455 455 kwargs['_info'] = info.codeinfo # fbo action_method
456 456 return wrapped
457 457
458 458
459 459 class jsonrpc_deprecated_method(object):
460 460 """
461 461 Marks method as deprecated, adds log.warning, and inject special key to
462 462 the request variable to mark method as deprecated.
463 463 Also injects special docstring that extract_docs will catch to mark
464 464 method as deprecated.
465 465
466 466 :param use_method: specify which method should be used instead of
467 467 the decorated one
468 468
469 469 Use like::
470 470
471 471 @jsonrpc_method()
472 472 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
473 473 def old_func(request, apiuser, arg1, arg2):
474 474 ...
475 475 """
476 476
477 477 def __init__(self, use_method, deprecated_at_version):
478 478 self.use_method = use_method
479 479 self.deprecated_at_version = deprecated_at_version
480 480 self.deprecated_msg = ''
481 481
482 482 def __call__(self, func):
483 483 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
484 484 method=self.use_method)
485 485
486 486 docstring = """\n
487 487 .. deprecated:: {version}
488 488
489 489 {deprecation_message}
490 490
491 491 {original_docstring}
492 492 """
493 493 func.__doc__ = docstring.format(
494 494 version=self.deprecated_at_version,
495 495 deprecation_message=self.deprecated_msg,
496 496 original_docstring=func.__doc__)
497 497 return decorator.decorator(self.__wrapper, func)
498 498
499 499 def __wrapper(self, func, *fargs, **fkwargs):
500 500 log.warning('DEPRECATED API CALL on function %s, please '
501 501 'use `%s` instead', func, self.use_method)
502 502 # alter function docstring to mark as deprecated, this is picked up
503 503 # via fabric file that generates API DOC.
504 504 result = func(*fargs, **fkwargs)
505 505
506 506 request = fargs[0]
507 507 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
508 508 return result
509 509
510 510
511 511 def includeme(config):
512 512 plugin_module = 'rhodecode.api'
513 513 plugin_settings = get_plugin_settings(
514 514 plugin_module, config.registry.settings)
515 515
516 516 if not hasattr(config.registry, 'jsonrpc_methods'):
517 517 config.registry.jsonrpc_methods = OrderedDict()
518 518
519 519 # match filter by given method only
520 520 config.add_view_predicate('jsonrpc_method', MethodPredicate)
521 521
522 522 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
523 523 serializer=json.dumps, indent=4))
524 524 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
525 525
526 526 config.add_route_predicate(
527 527 'jsonrpc_call', RoutePredicate)
528 528
529 529 config.add_route(
530 530 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
531 531
532 532 config.scan(plugin_module, ignore='rhodecode.api.tests')
533 533 # register some exception handling view
534 534 config.add_view(exception_view, context=JSONRPCBaseError)
535 535 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
536 536 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,140 +1,140 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 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 RhodeCode authentication token plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from sqlalchemy.ext.hybrid import hybrid_property
28 28
29 29 from rhodecode.translation import _
30 30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
31 31 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 32 from rhodecode.model.db import User, UserApiKeys
33 33
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def plugin_factory(plugin_id, *args, **kwds):
39 39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 40 return plugin
41 41
42 42
43 43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 44 pass
45 45
46 46
47 47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 48 """
49 49 Enables usage of authentication tokens for vcs operations.
50 50 """
51 51
52 52 def includeme(self, config):
53 53 config.add_authn_plugin(self)
54 54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
55 55 config.add_view(
56 56 'rhodecode.authentication.views.AuthnPluginViewBase',
57 57 attr='settings_get',
58 58 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
59 59 request_method='GET',
60 60 route_name='auth_home',
61 61 context=RhodecodeAuthnResource)
62 62 config.add_view(
63 63 'rhodecode.authentication.views.AuthnPluginViewBase',
64 64 attr='settings_post',
65 65 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
66 66 request_method='POST',
67 67 route_name='auth_home',
68 68 context=RhodecodeAuthnResource)
69 69
70 70 def get_display_name(self):
71 71 return _('Rhodecode Token Auth')
72 72
73 73 @hybrid_property
74 74 def name(self):
75 75 return "authtoken"
76 76
77 77 def user_activation_state(self):
78 78 def_user_perms = User.get_default_user().AuthUser.permissions['global']
79 79 return 'hg.register.auto_activate' in def_user_perms
80 80
81 81 def allows_authentication_from(
82 82 self, user, allows_non_existing_user=True,
83 83 allowed_auth_plugins=None, allowed_auth_sources=None):
84 84 """
85 85 Custom method for this auth that doesn't accept empty users. And also
86 86 allows users from all other active plugins to use it and also
87 87 authenticate against it. But only via vcs mode
88 88 """
89 89 from rhodecode.authentication.base import get_authn_registry
90 90 authn_registry = get_authn_registry()
91 91
92 92 active_plugins = set(
93 93 [x.name for x in authn_registry.get_plugins_for_authentication()])
94 94 active_plugins.discard(self.name)
95 95
96 96 allowed_auth_plugins = [self.name] + list(active_plugins)
97 97 # only for vcs operations
98 98 allowed_auth_sources = [VCS_TYPE]
99 99
100 100 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 101 user, allows_non_existing_user=False,
102 102 allowed_auth_plugins=allowed_auth_plugins,
103 103 allowed_auth_sources=allowed_auth_sources)
104 104
105 105 def auth(self, userobj, username, password, settings, **kwargs):
106 106 if not userobj:
107 107 log.debug('userobj was:%s skipping' % (userobj, ))
108 108 return None
109 109
110 110 user_attrs = {
111 111 "username": userobj.username,
112 112 "firstname": userobj.firstname,
113 113 "lastname": userobj.lastname,
114 114 "groups": [],
115 115 "email": userobj.email,
116 116 "admin": userobj.admin,
117 117 "active": userobj.active,
118 118 "active_from_extern": userobj.active,
119 119 "extern_name": userobj.user_id,
120 120 "extern_type": userobj.extern_type,
121 121 }
122 122
123 123 log.debug('Authenticating user with args %s', user_attrs)
124 124 if userobj.active:
125 role = UserApiKeys.ROLE_VCS
126 active_tokens = [x.api_key for x in
127 User.extra_valid_auth_tokens(userobj, role=role)]
128 if userobj.username == username and password in active_tokens:
125 token_match = userobj.authenticate_by_token(
126 password, roles=[UserApiKeys.ROLE_VCS])
127
128 if userobj.username == username and token_match:
129 129 log.info(
130 130 'user `%s` successfully authenticated via %s',
131 131 user_attrs['username'], self.name)
132 132 return user_attrs
133 133 log.error(
134 134 'user `%s` failed to authenticate via %s, reason: bad or '
135 135 'inactive token.', username, self.name)
136 136 else:
137 137 log.warning(
138 138 'user `%s` failed to authenticate via %s, reason: account not '
139 139 'active.', username, self.name)
140 140 return None
@@ -1,181 +1,180 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Feed controller for RhodeCode
23 23 """
24 24
25 25 import logging
26 26
27 27 import pytz
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 from beaker.cache import cache_region, region_invalidate
31 from beaker.cache import cache_region
32 32 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
33 33
34 from rhodecode.model.db import CacheKey
34 from rhodecode.model.db import CacheKey, UserApiKeys
35 35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import caches
37 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 37 from rhodecode.lib.base import BaseRepoController
39 38 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
40 39 from rhodecode.lib.utils2 import safe_int, str2bool
41 40 from rhodecode.lib.utils import PartialRenderer
42 41
43 42 log = logging.getLogger(__name__)
44 43
45 44
46 45 class FeedController(BaseRepoController):
47 46
48 47 def _get_config(self):
49 48 import rhodecode
50 49 config = rhodecode.CONFIG
51 50
52 51 return {
53 52 'language': 'en-us',
54 53 'feed_ttl': '5', # TTL of feed,
55 54 'feed_include_diff':
56 55 str2bool(config.get('rss_include_diff', False)),
57 56 'feed_items_per_page':
58 57 safe_int(config.get('rss_items_per_page', 20)),
59 58 'feed_diff_limit':
60 59 # we need to protect from parsing huge diffs here other way
61 60 # we can kill the server
62 61 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
63 62 }
64 63
65 @LoginRequired(auth_token_access=True)
64 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
66 65 def __before__(self):
67 66 super(FeedController, self).__before__()
68 67 config = self._get_config()
69 68 # common values for feeds
70 69 self.description = _('Changes on %s repository')
71 70 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
72 71 self.language = config["language"]
73 72 self.ttl = config["feed_ttl"]
74 73 self.feed_include_diff = config['feed_include_diff']
75 74 self.feed_diff_limit = config['feed_diff_limit']
76 75 self.feed_items_per_page = config['feed_items_per_page']
77 76
78 77 def __changes(self, commit):
79 78 diff_processor = DiffProcessor(
80 79 commit.diff(), diff_limit=self.feed_diff_limit)
81 80 _parsed = diff_processor.prepare(inline_diff=False)
82 81 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83 82
84 83 return _parsed, limited_diff
85 84
86 85 def _get_title(self, commit):
87 86 return h.shorter(commit.message, 160)
88 87
89 88 def _get_description(self, commit):
90 89 _renderer = PartialRenderer('feed/atom_feed_entry.mako')
91 90 parsed_diff, limited_diff = self.__changes(commit)
92 91 return _renderer(
93 92 'body',
94 93 commit=commit,
95 94 parsed_diff=parsed_diff,
96 95 limited_diff=limited_diff,
97 96 feed_include_diff=self.feed_include_diff,
98 97 )
99 98
100 99 def _set_timezone(self, date, tzinfo=pytz.utc):
101 100 if not getattr(date, "tzinfo", None):
102 101 date.replace(tzinfo=tzinfo)
103 102 return date
104 103
105 104 def _get_commits(self):
106 105 return list(c.rhodecode_repo[-self.feed_items_per_page:])
107 106
108 107 @HasRepoPermissionAnyDecorator(
109 108 'repository.read', 'repository.write', 'repository.admin')
110 109 def atom(self, repo_name):
111 110 """Produce an atom-1.0 feed via feedgenerator module"""
112 111
113 112 @cache_region('long_term')
114 113 def _generate_feed(cache_key):
115 114 feed = Atom1Feed(
116 115 title=self.title % repo_name,
117 116 link=url('summary_home', repo_name=repo_name, qualified=True),
118 117 description=self.description % repo_name,
119 118 language=self.language,
120 119 ttl=self.ttl
121 120 )
122 121
123 122 for commit in reversed(self._get_commits()):
124 123 date = self._set_timezone(commit.date)
125 124 feed.add_item(
126 125 title=self._get_title(commit),
127 126 author_name=commit.author,
128 127 description=self._get_description(commit),
129 128 link=url('changeset_home', repo_name=repo_name,
130 129 revision=commit.raw_id, qualified=True),
131 130 pubdate=date,)
132 131
133 132 return feed.mime_type, feed.writeString('utf-8')
134 133
135 134 invalidator_context = CacheKey.repo_context_cache(
136 135 _generate_feed, repo_name, CacheKey.CACHE_TYPE_ATOM)
137 136
138 137 with invalidator_context as context:
139 138 context.invalidate()
140 139 mime_type, feed = context.compute()
141 140
142 141 response.content_type = mime_type
143 142 return feed
144 143
145 144 @HasRepoPermissionAnyDecorator(
146 145 'repository.read', 'repository.write', 'repository.admin')
147 146 def rss(self, repo_name):
148 147 """Produce an rss2 feed via feedgenerator module"""
149 148
150 149 @cache_region('long_term')
151 150 def _generate_feed(cache_key):
152 151 feed = Rss201rev2Feed(
153 152 title=self.title % repo_name,
154 153 link=url('summary_home', repo_name=repo_name,
155 154 qualified=True),
156 155 description=self.description % repo_name,
157 156 language=self.language,
158 157 ttl=self.ttl
159 158 )
160 159
161 160 for commit in reversed(self._get_commits()):
162 161 date = self._set_timezone(commit.date)
163 162 feed.add_item(
164 163 title=self._get_title(commit),
165 164 author_name=commit.author,
166 165 description=self._get_description(commit),
167 166 link=url('changeset_home', repo_name=repo_name,
168 167 revision=commit.raw_id, qualified=True),
169 168 pubdate=date,)
170 169
171 170 return feed.mime_type, feed.writeString('utf-8')
172 171
173 172 invalidator_context = CacheKey.repo_context_cache(
174 173 _generate_feed, repo_name, CacheKey.CACHE_TYPE_RSS)
175 174
176 175 with invalidator_context as context:
177 176 context.invalidate()
178 177 mime_type, feed = context.compute()
179 178
180 179 response.content_type = mime_type
181 180 return feed
@@ -1,306 +1,306 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Journal / user event log controller for rhodecode
23 23 """
24 24
25 25 import logging
26 26 from itertools import groupby
27 27
28 28 from sqlalchemy import or_
29 29 from sqlalchemy.orm import joinedload
30 30
31 31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 32
33 33 from webob.exc import HTTPBadRequest
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.controllers.admin.admin import _journal_filter
38 from rhodecode.model.db import UserLog, UserFollowing, User
38 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
39 39 from rhodecode.model.meta import Session
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class JournalController(BaseController):
50 50
51 51 def __before__(self):
52 52 super(JournalController, self).__before__()
53 53 self.language = 'en-us'
54 54 self.ttl = "5"
55 55 self.feed_nr = 20
56 56 c.search_term = request.GET.get('filter')
57 57
58 58 def _get_daily_aggregate(self, journal):
59 59 groups = []
60 60 for k, g in groupby(journal, lambda x: x.action_as_day):
61 61 user_group = []
62 62 #groupby username if it's a present value, else fallback to journal username
63 63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
64 64 l = list(g2)
65 65 user_group.append((l[0].user, l))
66 66
67 67 groups.append((k, user_group,))
68 68
69 69 return groups
70 70
71 71 def _get_journal_data(self, following_repos):
72 72 repo_ids = [x.follows_repository.repo_id for x in following_repos
73 73 if x.follows_repository is not None]
74 74 user_ids = [x.follows_user.user_id for x in following_repos
75 75 if x.follows_user is not None]
76 76
77 77 filtering_criterion = None
78 78
79 79 if repo_ids and user_ids:
80 80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
81 81 UserLog.user_id.in_(user_ids))
82 82 if repo_ids and not user_ids:
83 83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
84 84 if not repo_ids and user_ids:
85 85 filtering_criterion = UserLog.user_id.in_(user_ids)
86 86 if filtering_criterion is not None:
87 87 journal = self.sa.query(UserLog)\
88 88 .options(joinedload(UserLog.user))\
89 89 .options(joinedload(UserLog.repository))
90 90 #filter
91 91 try:
92 92 journal = _journal_filter(journal, c.search_term)
93 93 except Exception:
94 94 # we want this to crash for now
95 95 raise
96 96 journal = journal.filter(filtering_criterion)\
97 97 .order_by(UserLog.action_date.desc())
98 98 else:
99 99 journal = []
100 100
101 101 return journal
102 102
103 103 def _atom_feed(self, repos, public=True):
104 104 journal = self._get_journal_data(repos)
105 105 if public:
106 106 _link = url('public_journal_atom', qualified=True)
107 107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
108 108 'atom feed')
109 109 else:
110 110 _link = url('journal_atom', qualified=True)
111 111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
112 112
113 113 feed = Atom1Feed(title=_desc,
114 114 link=_link,
115 115 description=_desc,
116 116 language=self.language,
117 117 ttl=self.ttl)
118 118
119 119 for entry in journal[:self.feed_nr]:
120 120 user = entry.user
121 121 if user is None:
122 122 #fix deleted users
123 123 user = AttributeDict({'short_contact': entry.username,
124 124 'email': '',
125 125 'full_contact': ''})
126 126 action, action_extra, ico = h.action_parser(entry, feed=True)
127 127 title = "%s - %s %s" % (user.short_contact, action(),
128 128 entry.repository.repo_name)
129 129 desc = action_extra()
130 130 _url = None
131 131 if entry.repository is not None:
132 132 _url = url('changelog_home',
133 133 repo_name=entry.repository.repo_name,
134 134 qualified=True)
135 135
136 136 feed.add_item(title=title,
137 137 pubdate=entry.action_date,
138 138 link=_url or url('', qualified=True),
139 139 author_email=user.email,
140 140 author_name=user.full_contact,
141 141 description=desc)
142 142
143 143 response.content_type = feed.mime_type
144 144 return feed.writeString('utf-8')
145 145
146 146 def _rss_feed(self, repos, public=True):
147 147 journal = self._get_journal_data(repos)
148 148 if public:
149 149 _link = url('public_journal_atom', qualified=True)
150 150 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
151 151 'rss feed')
152 152 else:
153 153 _link = url('journal_atom', qualified=True)
154 154 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
155 155
156 156 feed = Rss201rev2Feed(title=_desc,
157 157 link=_link,
158 158 description=_desc,
159 159 language=self.language,
160 160 ttl=self.ttl)
161 161
162 162 for entry in journal[:self.feed_nr]:
163 163 user = entry.user
164 164 if user is None:
165 165 #fix deleted users
166 166 user = AttributeDict({'short_contact': entry.username,
167 167 'email': '',
168 168 'full_contact': ''})
169 169 action, action_extra, ico = h.action_parser(entry, feed=True)
170 170 title = "%s - %s %s" % (user.short_contact, action(),
171 171 entry.repository.repo_name)
172 172 desc = action_extra()
173 173 _url = None
174 174 if entry.repository is not None:
175 175 _url = url('changelog_home',
176 176 repo_name=entry.repository.repo_name,
177 177 qualified=True)
178 178
179 179 feed.add_item(title=title,
180 180 pubdate=entry.action_date,
181 181 link=_url or url('', qualified=True),
182 182 author_email=user.email,
183 183 author_name=user.full_contact,
184 184 description=desc)
185 185
186 186 response.content_type = feed.mime_type
187 187 return feed.writeString('utf-8')
188 188
189 189 @LoginRequired()
190 190 @NotAnonymous()
191 191 def index(self):
192 192 # Return a rendered template
193 193 p = safe_int(request.GET.get('page', 1), 1)
194 194 c.user = User.get(c.rhodecode_user.user_id)
195 195 following = self.sa.query(UserFollowing)\
196 196 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
197 197 .options(joinedload(UserFollowing.follows_repository))\
198 198 .all()
199 199
200 200 journal = self._get_journal_data(following)
201 201
202 202 def url_generator(**kw):
203 203 return url.current(filter=c.search_term, **kw)
204 204
205 205 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
206 206 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
207 207
208 208 c.journal_data = render('journal/journal_data.mako')
209 209 if request.is_xhr:
210 210 return c.journal_data
211 211
212 212 return render('journal/journal.mako')
213 213
214 @LoginRequired(auth_token_access=True)
214 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
215 215 @NotAnonymous()
216 216 def journal_atom(self):
217 217 """
218 218 Produce an atom-1.0 feed via feedgenerator module
219 219 """
220 220 following = self.sa.query(UserFollowing)\
221 221 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
222 222 .options(joinedload(UserFollowing.follows_repository))\
223 223 .all()
224 224 return self._atom_feed(following, public=False)
225 225
226 @LoginRequired(auth_token_access=True)
226 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
227 227 @NotAnonymous()
228 228 def journal_rss(self):
229 229 """
230 230 Produce an rss feed via feedgenerator module
231 231 """
232 232 following = self.sa.query(UserFollowing)\
233 233 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
234 234 .options(joinedload(UserFollowing.follows_repository))\
235 235 .all()
236 236 return self._rss_feed(following, public=False)
237 237
238 238 @CSRFRequired()
239 239 @LoginRequired()
240 240 @NotAnonymous()
241 241 def toggle_following(self):
242 242 user_id = request.POST.get('follows_user_id')
243 243 if user_id:
244 244 try:
245 245 self.scm_model.toggle_following_user(
246 246 user_id, c.rhodecode_user.user_id)
247 247 Session().commit()
248 248 return 'ok'
249 249 except Exception:
250 250 raise HTTPBadRequest()
251 251
252 252 repo_id = request.POST.get('follows_repo_id')
253 253 if repo_id:
254 254 try:
255 255 self.scm_model.toggle_following_repo(
256 256 repo_id, c.rhodecode_user.user_id)
257 257 Session().commit()
258 258 return 'ok'
259 259 except Exception:
260 260 raise HTTPBadRequest()
261 261
262 262
263 263 @LoginRequired()
264 264 def public_journal(self):
265 265 # Return a rendered template
266 266 p = safe_int(request.GET.get('page', 1), 1)
267 267
268 268 c.following = self.sa.query(UserFollowing)\
269 269 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
270 270 .options(joinedload(UserFollowing.follows_repository))\
271 271 .all()
272 272
273 273 journal = self._get_journal_data(c.following)
274 274
275 275 c.journal_pager = Page(journal, page=p, items_per_page=20)
276 276
277 277 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
278 278
279 279 c.journal_data = render('journal/journal_data.mako')
280 280 if request.is_xhr:
281 281 return c.journal_data
282 282 return render('journal/public_journal.mako')
283 283
284 @LoginRequired(auth_token_access=True)
284 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
285 285 def public_journal_atom(self):
286 286 """
287 287 Produce an atom-1.0 feed via feedgenerator module
288 288 """
289 289 c.following = self.sa.query(UserFollowing)\
290 290 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
291 291 .options(joinedload(UserFollowing.follows_repository))\
292 292 .all()
293 293
294 294 return self._atom_feed(c.following)
295 295
296 @LoginRequired(auth_token_access=True)
296 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
297 297 def public_journal_rss(self):
298 298 """
299 299 Produce an rss2 feed via feedgenerator module
300 300 """
301 301 c.following = self.sa.query(UserFollowing)\
302 302 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
303 303 .options(joinedload(UserFollowing.follows_repository))\
304 304 .all()
305 305
306 306 return self._rss_feed(c.following)
@@ -1,1907 +1,1909 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 authentication and permission libraries
23 23 """
24 24
25 25 import inspect
26 26 import collections
27 27 import fnmatch
28 28 import hashlib
29 29 import itertools
30 30 import logging
31 31 import os
32 32 import random
33 33 import time
34 34 import traceback
35 35 from functools import wraps
36 36
37 37 import ipaddress
38 38 from pyramid.httpexceptions import HTTPForbidden
39 39 from pylons import url, request
40 40 from pylons.controllers.util import abort, redirect
41 41 from pylons.i18n.translation import _
42 42 from sqlalchemy import or_
43 43 from sqlalchemy.orm.exc import ObjectDeletedError
44 44 from sqlalchemy.orm import joinedload
45 45 from zope.cachedescriptors.property import Lazy as LazyProperty
46 46
47 47 import rhodecode
48 48 from rhodecode.model import meta
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.db import (
52 52 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
53 53 UserIpMap, UserApiKeys, RepoGroup)
54 54 from rhodecode.lib import caches
55 55 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
56 56 from rhodecode.lib.utils import (
57 57 get_repo_slug, get_repo_group_slug, get_user_group_slug)
58 58 from rhodecode.lib.caching_query import FromCache
59 59
60 60
61 61 if rhodecode.is_unix:
62 62 import bcrypt
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66 csrf_token_key = "csrf_token"
67 67
68 68
69 69 class PasswordGenerator(object):
70 70 """
71 71 This is a simple class for generating password from different sets of
72 72 characters
73 73 usage::
74 74
75 75 passwd_gen = PasswordGenerator()
76 76 #print 8-letter password containing only big and small letters
77 77 of alphabet
78 78 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
79 79 """
80 80 ALPHABETS_NUM = r'''1234567890'''
81 81 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
82 82 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
83 83 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
84 84 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
85 85 + ALPHABETS_NUM + ALPHABETS_SPECIAL
86 86 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
87 87 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
88 88 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
89 89 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
90 90
91 91 def __init__(self, passwd=''):
92 92 self.passwd = passwd
93 93
94 94 def gen_password(self, length, type_=None):
95 95 if type_ is None:
96 96 type_ = self.ALPHABETS_FULL
97 97 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
98 98 return self.passwd
99 99
100 100
101 101 class _RhodeCodeCryptoBase(object):
102 ENC_PREF = None
102 103
103 104 def hash_create(self, str_):
104 105 """
105 106 hash the string using
106 107
107 108 :param str_: password to hash
108 109 """
109 110 raise NotImplementedError
110 111
111 112 def hash_check_with_upgrade(self, password, hashed):
112 113 """
113 114 Returns tuple in which first element is boolean that states that
114 115 given password matches it's hashed version, and the second is new hash
115 116 of the password, in case this password should be migrated to new
116 117 cipher.
117 118 """
118 119 checked_hash = self.hash_check(password, hashed)
119 120 return checked_hash, None
120 121
121 122 def hash_check(self, password, hashed):
122 123 """
123 124 Checks matching password with it's hashed value.
124 125
125 126 :param password: password
126 127 :param hashed: password in hashed form
127 128 """
128 129 raise NotImplementedError
129 130
130 131 def _assert_bytes(self, value):
131 132 """
132 133 Passing in an `unicode` object can lead to hard to detect issues
133 134 if passwords contain non-ascii characters. Doing a type check
134 135 during runtime, so that such mistakes are detected early on.
135 136 """
136 137 if not isinstance(value, str):
137 138 raise TypeError(
138 139 "Bytestring required as input, got %r." % (value, ))
139 140
140 141
141 142 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
143 ENC_PREF = '$2a$10'
142 144
143 145 def hash_create(self, str_):
144 146 self._assert_bytes(str_)
145 147 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
146 148
147 149 def hash_check_with_upgrade(self, password, hashed):
148 150 """
149 151 Returns tuple in which first element is boolean that states that
150 152 given password matches it's hashed version, and the second is new hash
151 153 of the password, in case this password should be migrated to new
152 154 cipher.
153 155
154 156 This implements special upgrade logic which works like that:
155 157 - check if the given password == bcrypted hash, if yes then we
156 158 properly used password and it was already in bcrypt. Proceed
157 159 without any changes
158 160 - if bcrypt hash check is not working try with sha256. If hash compare
159 161 is ok, it means we using correct but old hashed password. indicate
160 162 hash change and proceed
161 163 """
162 164
163 165 new_hash = None
164 166
165 167 # regular pw check
166 168 password_match_bcrypt = self.hash_check(password, hashed)
167 169
168 170 # now we want to know if the password was maybe from sha256
169 171 # basically calling _RhodeCodeCryptoSha256().hash_check()
170 172 if not password_match_bcrypt:
171 173 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
172 174 new_hash = self.hash_create(password) # make new bcrypt hash
173 175 password_match_bcrypt = True
174 176
175 177 return password_match_bcrypt, new_hash
176 178
177 179 def hash_check(self, password, hashed):
178 180 """
179 181 Checks matching password with it's hashed value.
180 182
181 183 :param password: password
182 184 :param hashed: password in hashed form
183 185 """
184 186 self._assert_bytes(password)
185 187 try:
186 188 return bcrypt.hashpw(password, hashed) == hashed
187 189 except ValueError as e:
188 190 # we're having a invalid salt here probably, we should not crash
189 191 # just return with False as it would be a wrong password.
190 192 log.debug('Failed to check password hash using bcrypt %s',
191 193 safe_str(e))
192 194
193 195 return False
194 196
195 197
196 198 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
199 ENC_PREF = '_'
197 200
198 201 def hash_create(self, str_):
199 202 self._assert_bytes(str_)
200 203 return hashlib.sha256(str_).hexdigest()
201 204
202 205 def hash_check(self, password, hashed):
203 206 """
204 207 Checks matching password with it's hashed value.
205 208
206 209 :param password: password
207 210 :param hashed: password in hashed form
208 211 """
209 212 self._assert_bytes(password)
210 213 return hashlib.sha256(password).hexdigest() == hashed
211 214
212 215
213 216 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
217 ENC_PREF = '_'
214 218
215 219 def hash_create(self, str_):
216 220 self._assert_bytes(str_)
217 221 return hashlib.md5(str_).hexdigest()
218 222
219 223 def hash_check(self, password, hashed):
220 224 """
221 225 Checks matching password with it's hashed value.
222 226
223 227 :param password: password
224 228 :param hashed: password in hashed form
225 229 """
226 230 self._assert_bytes(password)
227 231 return hashlib.md5(password).hexdigest() == hashed
228 232
229 233
230 234 def crypto_backend():
231 235 """
232 236 Return the matching crypto backend.
233 237
234 238 Selection is based on if we run tests or not, we pick md5 backend to run
235 239 tests faster since BCRYPT is expensive to calculate
236 240 """
237 241 if rhodecode.is_test:
238 242 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
239 243 else:
240 244 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 245
242 246 return RhodeCodeCrypto
243 247
244 248
245 249 def get_crypt_password(password):
246 250 """
247 251 Create the hash of `password` with the active crypto backend.
248 252
249 253 :param password: The cleartext password.
250 254 :type password: unicode
251 255 """
252 256 password = safe_str(password)
253 257 return crypto_backend().hash_create(password)
254 258
255 259
256 260 def check_password(password, hashed):
257 261 """
258 262 Check if the value in `password` matches the hash in `hashed`.
259 263
260 264 :param password: The cleartext password.
261 265 :type password: unicode
262 266
263 267 :param hashed: The expected hashed version of the password.
264 268 :type hashed: The hash has to be passed in in text representation.
265 269 """
266 270 password = safe_str(password)
267 271 return crypto_backend().hash_check(password, hashed)
268 272
269 273
270 274 def generate_auth_token(data, salt=None):
271 275 """
272 276 Generates API KEY from given string
273 277 """
274 278
275 279 if salt is None:
276 280 salt = os.urandom(16)
277 281 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 282
279 283
280 284 class CookieStoreWrapper(object):
281 285
282 286 def __init__(self, cookie_store):
283 287 self.cookie_store = cookie_store
284 288
285 289 def __repr__(self):
286 290 return 'CookieStore<%s>' % (self.cookie_store)
287 291
288 292 def get(self, key, other=None):
289 293 if isinstance(self.cookie_store, dict):
290 294 return self.cookie_store.get(key, other)
291 295 elif isinstance(self.cookie_store, AuthUser):
292 296 return self.cookie_store.__dict__.get(key, other)
293 297
294 298
295 299 def _cached_perms_data(user_id, scope, user_is_admin,
296 300 user_inherit_default_permissions, explicit, algo):
297 301
298 302 permissions = PermissionCalculator(
299 303 user_id, scope, user_is_admin, user_inherit_default_permissions,
300 304 explicit, algo)
301 305 return permissions.calculate()
302 306
303 307 class PermOrigin:
304 308 ADMIN = 'superadmin'
305 309
306 310 REPO_USER = 'user:%s'
307 311 REPO_USERGROUP = 'usergroup:%s'
308 312 REPO_OWNER = 'repo.owner'
309 313 REPO_DEFAULT = 'repo.default'
310 314 REPO_PRIVATE = 'repo.private'
311 315
312 316 REPOGROUP_USER = 'user:%s'
313 317 REPOGROUP_USERGROUP = 'usergroup:%s'
314 318 REPOGROUP_OWNER = 'group.owner'
315 319 REPOGROUP_DEFAULT = 'group.default'
316 320
317 321 USERGROUP_USER = 'user:%s'
318 322 USERGROUP_USERGROUP = 'usergroup:%s'
319 323 USERGROUP_OWNER = 'usergroup.owner'
320 324 USERGROUP_DEFAULT = 'usergroup.default'
321 325
322 326
323 327 class PermOriginDict(dict):
324 328 """
325 329 A special dict used for tracking permissions along with their origins.
326 330
327 331 `__setitem__` has been overridden to expect a tuple(perm, origin)
328 332 `__getitem__` will return only the perm
329 333 `.perm_origin_stack` will return the stack of (perm, origin) set per key
330 334
331 335 >>> perms = PermOriginDict()
332 336 >>> perms['resource'] = 'read', 'default'
333 337 >>> perms['resource']
334 338 'read'
335 339 >>> perms['resource'] = 'write', 'admin'
336 340 >>> perms['resource']
337 341 'write'
338 342 >>> perms.perm_origin_stack
339 343 {'resource': [('read', 'default'), ('write', 'admin')]}
340 344 """
341 345
342 346
343 347 def __init__(self, *args, **kw):
344 348 dict.__init__(self, *args, **kw)
345 349 self.perm_origin_stack = {}
346 350
347 351 def __setitem__(self, key, (perm, origin)):
348 352 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
349 353 dict.__setitem__(self, key, perm)
350 354
351 355
352 356 class PermissionCalculator(object):
353 357
354 358 def __init__(
355 359 self, user_id, scope, user_is_admin,
356 360 user_inherit_default_permissions, explicit, algo):
357 361 self.user_id = user_id
358 362 self.user_is_admin = user_is_admin
359 363 self.inherit_default_permissions = user_inherit_default_permissions
360 364 self.explicit = explicit
361 365 self.algo = algo
362 366
363 367 scope = scope or {}
364 368 self.scope_repo_id = scope.get('repo_id')
365 369 self.scope_repo_group_id = scope.get('repo_group_id')
366 370 self.scope_user_group_id = scope.get('user_group_id')
367 371
368 372 self.default_user_id = User.get_default_user(cache=True).user_id
369 373
370 374 self.permissions_repositories = PermOriginDict()
371 375 self.permissions_repository_groups = PermOriginDict()
372 376 self.permissions_user_groups = PermOriginDict()
373 377 self.permissions_global = set()
374 378
375 379 self.default_repo_perms = Permission.get_default_repo_perms(
376 380 self.default_user_id, self.scope_repo_id)
377 381 self.default_repo_groups_perms = Permission.get_default_group_perms(
378 382 self.default_user_id, self.scope_repo_group_id)
379 383 self.default_user_group_perms = \
380 384 Permission.get_default_user_group_perms(
381 385 self.default_user_id, self.scope_user_group_id)
382 386
383 387 def calculate(self):
384 388 if self.user_is_admin:
385 389 return self._admin_permissions()
386 390
387 391 self._calculate_global_default_permissions()
388 392 self._calculate_global_permissions()
389 393 self._calculate_default_permissions()
390 394 self._calculate_repository_permissions()
391 395 self._calculate_repository_group_permissions()
392 396 self._calculate_user_group_permissions()
393 397 return self._permission_structure()
394 398
395 399 def _admin_permissions(self):
396 400 """
397 401 admin user have all default rights for repositories
398 402 and groups set to admin
399 403 """
400 404 self.permissions_global.add('hg.admin')
401 405 self.permissions_global.add('hg.create.write_on_repogroup.true')
402 406
403 407 # repositories
404 408 for perm in self.default_repo_perms:
405 409 r_k = perm.UserRepoToPerm.repository.repo_name
406 410 p = 'repository.admin'
407 411 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
408 412
409 413 # repository groups
410 414 for perm in self.default_repo_groups_perms:
411 415 rg_k = perm.UserRepoGroupToPerm.group.group_name
412 416 p = 'group.admin'
413 417 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
414 418
415 419 # user groups
416 420 for perm in self.default_user_group_perms:
417 421 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
418 422 p = 'usergroup.admin'
419 423 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
420 424
421 425 return self._permission_structure()
422 426
423 427 def _calculate_global_default_permissions(self):
424 428 """
425 429 global permissions taken from the default user
426 430 """
427 431 default_global_perms = UserToPerm.query()\
428 432 .filter(UserToPerm.user_id == self.default_user_id)\
429 433 .options(joinedload(UserToPerm.permission))
430 434
431 435 for perm in default_global_perms:
432 436 self.permissions_global.add(perm.permission.permission_name)
433 437
434 438 def _calculate_global_permissions(self):
435 439 """
436 440 Set global system permissions with user permissions or permissions
437 441 taken from the user groups of the current user.
438 442
439 443 The permissions include repo creating, repo group creating, forking
440 444 etc.
441 445 """
442 446
443 447 # now we read the defined permissions and overwrite what we have set
444 448 # before those can be configured from groups or users explicitly.
445 449
446 450 # TODO: johbo: This seems to be out of sync, find out the reason
447 451 # for the comment below and update it.
448 452
449 453 # In case we want to extend this list we should be always in sync with
450 454 # User.DEFAULT_USER_PERMISSIONS definitions
451 455 _configurable = frozenset([
452 456 'hg.fork.none', 'hg.fork.repository',
453 457 'hg.create.none', 'hg.create.repository',
454 458 'hg.usergroup.create.false', 'hg.usergroup.create.true',
455 459 'hg.repogroup.create.false', 'hg.repogroup.create.true',
456 460 'hg.create.write_on_repogroup.false',
457 461 'hg.create.write_on_repogroup.true',
458 462 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
459 463 ])
460 464
461 465 # USER GROUPS comes first user group global permissions
462 466 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
463 467 .options(joinedload(UserGroupToPerm.permission))\
464 468 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
465 469 UserGroupMember.users_group_id))\
466 470 .filter(UserGroupMember.user_id == self.user_id)\
467 471 .order_by(UserGroupToPerm.users_group_id)\
468 472 .all()
469 473
470 474 # need to group here by groups since user can be in more than
471 475 # one group, so we get all groups
472 476 _explicit_grouped_perms = [
473 477 [x, list(y)] for x, y in
474 478 itertools.groupby(user_perms_from_users_groups,
475 479 lambda _x: _x.users_group)]
476 480
477 481 for gr, perms in _explicit_grouped_perms:
478 482 # since user can be in multiple groups iterate over them and
479 483 # select the lowest permissions first (more explicit)
480 484 # TODO: marcink: do this^^
481 485
482 486 # group doesn't inherit default permissions so we actually set them
483 487 if not gr.inherit_default_permissions:
484 488 # NEED TO IGNORE all previously set configurable permissions
485 489 # and replace them with explicitly set from this user
486 490 # group permissions
487 491 self.permissions_global = self.permissions_global.difference(
488 492 _configurable)
489 493 for perm in perms:
490 494 self.permissions_global.add(perm.permission.permission_name)
491 495
492 496 # user explicit global permissions
493 497 user_perms = Session().query(UserToPerm)\
494 498 .options(joinedload(UserToPerm.permission))\
495 499 .filter(UserToPerm.user_id == self.user_id).all()
496 500
497 501 if not self.inherit_default_permissions:
498 502 # NEED TO IGNORE all configurable permissions and
499 503 # replace them with explicitly set from this user permissions
500 504 self.permissions_global = self.permissions_global.difference(
501 505 _configurable)
502 506 for perm in user_perms:
503 507 self.permissions_global.add(perm.permission.permission_name)
504 508
505 509 def _calculate_default_permissions(self):
506 510 """
507 511 Set default user permissions for repositories, repository groups
508 512 taken from the default user.
509 513
510 514 Calculate inheritance of object permissions based on what we have now
511 515 in GLOBAL permissions. We check if .false is in GLOBAL since this is
512 516 explicitly set. Inherit is the opposite of .false being there.
513 517
514 518 .. note::
515 519
516 520 the syntax is little bit odd but what we need to check here is
517 521 the opposite of .false permission being in the list so even for
518 522 inconsistent state when both .true/.false is there
519 523 .false is more important
520 524
521 525 """
522 526 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
523 527 in self.permissions_global)
524 528
525 529 # defaults for repositories, taken from `default` user permissions
526 530 # on given repo
527 531 for perm in self.default_repo_perms:
528 532 r_k = perm.UserRepoToPerm.repository.repo_name
529 533 o = PermOrigin.REPO_DEFAULT
530 534 if perm.Repository.private and not (
531 535 perm.Repository.user_id == self.user_id):
532 536 # disable defaults for private repos,
533 537 p = 'repository.none'
534 538 o = PermOrigin.REPO_PRIVATE
535 539 elif perm.Repository.user_id == self.user_id:
536 540 # set admin if owner
537 541 p = 'repository.admin'
538 542 o = PermOrigin.REPO_OWNER
539 543 else:
540 544 p = perm.Permission.permission_name
541 545 # if we decide this user isn't inheriting permissions from
542 546 # default user we set him to .none so only explicit
543 547 # permissions work
544 548 if not user_inherit_object_permissions:
545 549 p = 'repository.none'
546 550 self.permissions_repositories[r_k] = p, o
547 551
548 552 # defaults for repository groups taken from `default` user permission
549 553 # on given group
550 554 for perm in self.default_repo_groups_perms:
551 555 rg_k = perm.UserRepoGroupToPerm.group.group_name
552 556 o = PermOrigin.REPOGROUP_DEFAULT
553 557 if perm.RepoGroup.user_id == self.user_id:
554 558 # set admin if owner
555 559 p = 'group.admin'
556 560 o = PermOrigin.REPOGROUP_OWNER
557 561 else:
558 562 p = perm.Permission.permission_name
559 563
560 564 # if we decide this user isn't inheriting permissions from default
561 565 # user we set him to .none so only explicit permissions work
562 566 if not user_inherit_object_permissions:
563 567 p = 'group.none'
564 568 self.permissions_repository_groups[rg_k] = p, o
565 569
566 570 # defaults for user groups taken from `default` user permission
567 571 # on given user group
568 572 for perm in self.default_user_group_perms:
569 573 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
570 574 p = perm.Permission.permission_name
571 575 o = PermOrigin.USERGROUP_DEFAULT
572 576 # if we decide this user isn't inheriting permissions from default
573 577 # user we set him to .none so only explicit permissions work
574 578 if not user_inherit_object_permissions:
575 579 p = 'usergroup.none'
576 580 self.permissions_user_groups[u_k] = p, o
577 581
578 582 def _calculate_repository_permissions(self):
579 583 """
580 584 Repository permissions for the current user.
581 585
582 586 Check if the user is part of user groups for this repository and
583 587 fill in the permission from it. `_choose_permission` decides of which
584 588 permission should be selected based on selected method.
585 589 """
586 590
587 591 # user group for repositories permissions
588 592 user_repo_perms_from_user_group = Permission\
589 593 .get_default_repo_perms_from_user_group(
590 594 self.user_id, self.scope_repo_id)
591 595
592 596 multiple_counter = collections.defaultdict(int)
593 597 for perm in user_repo_perms_from_user_group:
594 598 r_k = perm.UserGroupRepoToPerm.repository.repo_name
595 599 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
596 600 multiple_counter[r_k] += 1
597 601 p = perm.Permission.permission_name
598 602 o = PermOrigin.REPO_USERGROUP % ug_k
599 603
600 604 if perm.Repository.user_id == self.user_id:
601 605 # set admin if owner
602 606 p = 'repository.admin'
603 607 o = PermOrigin.REPO_OWNER
604 608 else:
605 609 if multiple_counter[r_k] > 1:
606 610 cur_perm = self.permissions_repositories[r_k]
607 611 p = self._choose_permission(p, cur_perm)
608 612 self.permissions_repositories[r_k] = p, o
609 613
610 614 # user explicit permissions for repositories, overrides any specified
611 615 # by the group permission
612 616 user_repo_perms = Permission.get_default_repo_perms(
613 617 self.user_id, self.scope_repo_id)
614 618 for perm in user_repo_perms:
615 619 r_k = perm.UserRepoToPerm.repository.repo_name
616 620 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
617 621 # set admin if owner
618 622 if perm.Repository.user_id == self.user_id:
619 623 p = 'repository.admin'
620 624 o = PermOrigin.REPO_OWNER
621 625 else:
622 626 p = perm.Permission.permission_name
623 627 if not self.explicit:
624 628 cur_perm = self.permissions_repositories.get(
625 629 r_k, 'repository.none')
626 630 p = self._choose_permission(p, cur_perm)
627 631 self.permissions_repositories[r_k] = p, o
628 632
629 633 def _calculate_repository_group_permissions(self):
630 634 """
631 635 Repository group permissions for the current user.
632 636
633 637 Check if the user is part of user groups for repository groups and
634 638 fill in the permissions from it. `_choose_permmission` decides of which
635 639 permission should be selected based on selected method.
636 640 """
637 641 # user group for repo groups permissions
638 642 user_repo_group_perms_from_user_group = Permission\
639 643 .get_default_group_perms_from_user_group(
640 644 self.user_id, self.scope_repo_group_id)
641 645
642 646 multiple_counter = collections.defaultdict(int)
643 647 for perm in user_repo_group_perms_from_user_group:
644 648 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
645 649 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
646 650 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
647 651 multiple_counter[g_k] += 1
648 652 p = perm.Permission.permission_name
649 653 if perm.RepoGroup.user_id == self.user_id:
650 654 # set admin if owner
651 655 p = 'group.admin'
652 656 o = PermOrigin.REPOGROUP_OWNER
653 657 else:
654 658 if multiple_counter[g_k] > 1:
655 659 cur_perm = self.permissions_repository_groups[g_k]
656 660 p = self._choose_permission(p, cur_perm)
657 661 self.permissions_repository_groups[g_k] = p, o
658 662
659 663 # user explicit permissions for repository groups
660 664 user_repo_groups_perms = Permission.get_default_group_perms(
661 665 self.user_id, self.scope_repo_group_id)
662 666 for perm in user_repo_groups_perms:
663 667 rg_k = perm.UserRepoGroupToPerm.group.group_name
664 668 u_k = perm.UserRepoGroupToPerm.user.username
665 669 o = PermOrigin.REPOGROUP_USER % u_k
666 670
667 671 if perm.RepoGroup.user_id == self.user_id:
668 672 # set admin if owner
669 673 p = 'group.admin'
670 674 o = PermOrigin.REPOGROUP_OWNER
671 675 else:
672 676 p = perm.Permission.permission_name
673 677 if not self.explicit:
674 678 cur_perm = self.permissions_repository_groups.get(
675 679 rg_k, 'group.none')
676 680 p = self._choose_permission(p, cur_perm)
677 681 self.permissions_repository_groups[rg_k] = p, o
678 682
679 683 def _calculate_user_group_permissions(self):
680 684 """
681 685 User group permissions for the current user.
682 686 """
683 687 # user group for user group permissions
684 688 user_group_from_user_group = Permission\
685 689 .get_default_user_group_perms_from_user_group(
686 690 self.user_id, self.scope_repo_group_id)
687 691
688 692 multiple_counter = collections.defaultdict(int)
689 693 for perm in user_group_from_user_group:
690 694 g_k = perm.UserGroupUserGroupToPerm\
691 695 .target_user_group.users_group_name
692 696 u_k = perm.UserGroupUserGroupToPerm\
693 697 .user_group.users_group_name
694 698 o = PermOrigin.USERGROUP_USERGROUP % u_k
695 699 multiple_counter[g_k] += 1
696 700 p = perm.Permission.permission_name
697 701 if multiple_counter[g_k] > 1:
698 702 cur_perm = self.permissions_user_groups[g_k]
699 703 p = self._choose_permission(p, cur_perm)
700 704 self.permissions_user_groups[g_k] = p, o
701 705
702 706 # user explicit permission for user groups
703 707 user_user_groups_perms = Permission.get_default_user_group_perms(
704 708 self.user_id, self.scope_user_group_id)
705 709 for perm in user_user_groups_perms:
706 710 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
707 711 u_k = perm.UserUserGroupToPerm.user.username
708 712 p = perm.Permission.permission_name
709 713 o = PermOrigin.USERGROUP_USER % u_k
710 714 if not self.explicit:
711 715 cur_perm = self.permissions_user_groups.get(
712 716 ug_k, 'usergroup.none')
713 717 p = self._choose_permission(p, cur_perm)
714 718 self.permissions_user_groups[ug_k] = p, o
715 719
716 720 def _choose_permission(self, new_perm, cur_perm):
717 721 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
718 722 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
719 723 if self.algo == 'higherwin':
720 724 if new_perm_val > cur_perm_val:
721 725 return new_perm
722 726 return cur_perm
723 727 elif self.algo == 'lowerwin':
724 728 if new_perm_val < cur_perm_val:
725 729 return new_perm
726 730 return cur_perm
727 731
728 732 def _permission_structure(self):
729 733 return {
730 734 'global': self.permissions_global,
731 735 'repositories': self.permissions_repositories,
732 736 'repositories_groups': self.permissions_repository_groups,
733 737 'user_groups': self.permissions_user_groups,
734 738 }
735 739
736 740
737 741 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
738 742 """
739 743 Check if given controller_name is in whitelist of auth token access
740 744 """
741 745 if not whitelist:
742 746 from rhodecode import CONFIG
743 747 whitelist = aslist(
744 748 CONFIG.get('api_access_controllers_whitelist'), sep=',')
745 749 log.debug(
746 750 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
747 751
748 752 auth_token_access_valid = False
749 753 for entry in whitelist:
750 754 if fnmatch.fnmatch(controller_name, entry):
751 755 auth_token_access_valid = True
752 756 break
753 757
754 758 if auth_token_access_valid:
755 759 log.debug('controller:%s matches entry in whitelist'
756 760 % (controller_name,))
757 761 else:
758 762 msg = ('controller: %s does *NOT* match any entry in whitelist'
759 763 % (controller_name,))
760 764 if auth_token:
761 765 # if we use auth token key and don't have access it's a warning
762 766 log.warning(msg)
763 767 else:
764 768 log.debug(msg)
765 769
766 770 return auth_token_access_valid
767 771
768 772
769 773 class AuthUser(object):
770 774 """
771 775 A simple object that handles all attributes of user in RhodeCode
772 776
773 777 It does lookup based on API key,given user, or user present in session
774 778 Then it fills all required information for such user. It also checks if
775 779 anonymous access is enabled and if so, it returns default user as logged in
776 780 """
777 781 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
778 782
779 783 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
780 784
781 785 self.user_id = user_id
782 786 self._api_key = api_key
783 787
784 788 self.api_key = None
785 789 self.feed_token = ''
786 790 self.username = username
787 791 self.ip_addr = ip_addr
788 792 self.name = ''
789 793 self.lastname = ''
790 794 self.email = ''
791 795 self.is_authenticated = False
792 796 self.admin = False
793 797 self.inherit_default_permissions = False
794 798 self.password = ''
795 799
796 800 self.anonymous_user = None # propagated on propagate_data
797 801 self.propagate_data()
798 802 self._instance = None
799 803 self._permissions_scoped_cache = {} # used to bind scoped calculation
800 804
801 805 @LazyProperty
802 806 def permissions(self):
803 807 return self.get_perms(user=self, cache=False)
804 808
805 809 def permissions_with_scope(self, scope):
806 810 """
807 811 Call the get_perms function with scoped data. The scope in that function
808 812 narrows the SQL calls to the given ID of objects resulting in fetching
809 813 Just particular permission we want to obtain. If scope is an empty dict
810 814 then it basically narrows the scope to GLOBAL permissions only.
811 815
812 816 :param scope: dict
813 817 """
814 818 if 'repo_name' in scope:
815 819 obj = Repository.get_by_repo_name(scope['repo_name'])
816 820 if obj:
817 821 scope['repo_id'] = obj.repo_id
818 822 _scope = {
819 823 'repo_id': -1,
820 824 'user_group_id': -1,
821 825 'repo_group_id': -1,
822 826 }
823 827 _scope.update(scope)
824 828 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
825 829 _scope.items())))
826 830 if cache_key not in self._permissions_scoped_cache:
827 831 # store in cache to mimic how the @LazyProperty works,
828 832 # the difference here is that we use the unique key calculated
829 833 # from params and values
830 834 res = self.get_perms(user=self, cache=False, scope=_scope)
831 835 self._permissions_scoped_cache[cache_key] = res
832 836 return self._permissions_scoped_cache[cache_key]
833 837
834 @property
835 def auth_tokens(self):
836 return self.get_auth_tokens()
837
838 838 def get_instance(self):
839 839 return User.get(self.user_id)
840 840
841 841 def update_lastactivity(self):
842 842 if self.user_id:
843 843 User.get(self.user_id).update_lastactivity()
844 844
845 845 def propagate_data(self):
846 846 """
847 847 Fills in user data and propagates values to this instance. Maps fetched
848 848 user attributes to this class instance attributes
849 849 """
850 850 log.debug('starting data propagation for new potential AuthUser')
851 851 user_model = UserModel()
852 852 anon_user = self.anonymous_user = User.get_default_user(cache=True)
853 853 is_user_loaded = False
854 854
855 855 # lookup by userid
856 856 if self.user_id is not None and self.user_id != anon_user.user_id:
857 857 log.debug('Trying Auth User lookup by USER ID: `%s`' % self.user_id)
858 858 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
859 859
860 860 # try go get user by api key
861 861 elif self._api_key and self._api_key != anon_user.api_key:
862 862 log.debug('Trying Auth User lookup by API KEY: `%s`' % self._api_key)
863 863 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
864 864
865 865 # lookup by username
866 866 elif self.username:
867 867 log.debug('Trying Auth User lookup by USER NAME: `%s`' % self.username)
868 868 is_user_loaded = user_model.fill_data(self, username=self.username)
869 869 else:
870 870 log.debug('No data in %s that could been used to log in' % self)
871 871
872 872 if not is_user_loaded:
873 873 log.debug('Failed to load user. Fallback to default user')
874 874 # if we cannot authenticate user try anonymous
875 875 if anon_user.active:
876 876 user_model.fill_data(self, user_id=anon_user.user_id)
877 877 # then we set this user is logged in
878 878 self.is_authenticated = True
879 879 else:
880 880 # in case of disabled anonymous user we reset some of the
881 881 # parameters so such user is "corrupted", skipping the fill_data
882 882 for attr in ['user_id', 'username', 'admin', 'active']:
883 883 setattr(self, attr, None)
884 884 self.is_authenticated = False
885 885
886 886 if not self.username:
887 887 self.username = 'None'
888 888
889 889 log.debug('Auth User is now %s' % self)
890 890
891 891 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
892 892 cache=False):
893 893 """
894 894 Fills user permission attribute with permissions taken from database
895 895 works for permissions given for repositories, and for permissions that
896 896 are granted to groups
897 897
898 898 :param user: instance of User object from database
899 899 :param explicit: In case there are permissions both for user and a group
900 900 that user is part of, explicit flag will defiine if user will
901 901 explicitly override permissions from group, if it's False it will
902 902 make decision based on the algo
903 903 :param algo: algorithm to decide what permission should be choose if
904 904 it's multiple defined, eg user in two different groups. It also
905 905 decides if explicit flag is turned off how to specify the permission
906 906 for case when user is in a group + have defined separate permission
907 907 """
908 908 user_id = user.user_id
909 909 user_is_admin = user.is_admin
910 910
911 911 # inheritance of global permissions like create repo/fork repo etc
912 912 user_inherit_default_permissions = user.inherit_default_permissions
913 913
914 914 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
915 915 compute = caches.conditional_cache(
916 916 'short_term', 'cache_desc',
917 917 condition=cache, func=_cached_perms_data)
918 918 result = compute(user_id, scope, user_is_admin,
919 919 user_inherit_default_permissions, explicit, algo)
920 920
921 921 result_repr = []
922 922 for k in result:
923 923 result_repr.append((k, len(result[k])))
924 924
925 925 log.debug('PERMISSION tree computed %s' % (result_repr,))
926 926 return result
927 927
928 def get_auth_tokens(self):
929 auth_tokens = [self.api_key]
930 for api_key in UserApiKeys.query()\
931 .filter(UserApiKeys.user_id == self.user_id)\
932 .filter(or_(UserApiKeys.expires == -1,
933 UserApiKeys.expires >= time.time())).all():
934 auth_tokens.append(api_key.api_key)
935
936 return auth_tokens
937
938 928 @property
939 929 def is_default(self):
940 930 return self.username == User.DEFAULT_USER
941 931
942 932 @property
943 933 def is_admin(self):
944 934 return self.admin
945 935
946 936 @property
947 937 def is_user_object(self):
948 938 return self.user_id is not None
949 939
950 940 @property
951 941 def repositories_admin(self):
952 942 """
953 943 Returns list of repositories you're an admin of
954 944 """
955 945 return [x[0] for x in self.permissions['repositories'].iteritems()
956 946 if x[1] == 'repository.admin']
957 947
958 948 @property
959 949 def repository_groups_admin(self):
960 950 """
961 951 Returns list of repository groups you're an admin of
962 952 """
963 953 return [x[0]
964 954 for x in self.permissions['repositories_groups'].iteritems()
965 955 if x[1] == 'group.admin']
966 956
967 957 @property
968 958 def user_groups_admin(self):
969 959 """
970 960 Returns list of user groups you're an admin of
971 961 """
972 962 return [x[0] for x in self.permissions['user_groups'].iteritems()
973 963 if x[1] == 'usergroup.admin']
974 964
975 965 @property
976 966 def ip_allowed(self):
977 967 """
978 968 Checks if ip_addr used in constructor is allowed from defined list of
979 969 allowed ip_addresses for user
980 970
981 971 :returns: boolean, True if ip is in allowed ip range
982 972 """
983 973 # check IP
984 974 inherit = self.inherit_default_permissions
985 975 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
986 976 inherit_from_default=inherit)
987 977 @property
988 978 def personal_repo_group(self):
989 979 return RepoGroup.get_user_personal_repo_group(self.user_id)
990 980
991 981 @classmethod
992 982 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
993 983 allowed_ips = AuthUser.get_allowed_ips(
994 984 user_id, cache=True, inherit_from_default=inherit_from_default)
995 985 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
996 986 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
997 987 return True
998 988 else:
999 989 log.info('Access for IP:%s forbidden, '
1000 990 'not in %s' % (ip_addr, allowed_ips))
1001 991 return False
1002 992
1003 993 def __repr__(self):
1004 994 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1005 995 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1006 996
1007 997 def set_authenticated(self, authenticated=True):
1008 998 if self.user_id != self.anonymous_user.user_id:
1009 999 self.is_authenticated = authenticated
1010 1000
1011 1001 def get_cookie_store(self):
1012 1002 return {
1013 1003 'username': self.username,
1014 1004 'password': md5(self.password),
1015 1005 'user_id': self.user_id,
1016 1006 'is_authenticated': self.is_authenticated
1017 1007 }
1018 1008
1019 1009 @classmethod
1020 1010 def from_cookie_store(cls, cookie_store):
1021 1011 """
1022 1012 Creates AuthUser from a cookie store
1023 1013
1024 1014 :param cls:
1025 1015 :param cookie_store:
1026 1016 """
1027 1017 user_id = cookie_store.get('user_id')
1028 1018 username = cookie_store.get('username')
1029 1019 api_key = cookie_store.get('api_key')
1030 1020 return AuthUser(user_id, api_key, username)
1031 1021
1032 1022 @classmethod
1033 1023 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1034 1024 _set = set()
1035 1025
1036 1026 if inherit_from_default:
1037 1027 default_ips = UserIpMap.query().filter(
1038 1028 UserIpMap.user == User.get_default_user(cache=True))
1039 1029 if cache:
1040 1030 default_ips = default_ips.options(FromCache("sql_cache_short",
1041 1031 "get_user_ips_default"))
1042 1032
1043 1033 # populate from default user
1044 1034 for ip in default_ips:
1045 1035 try:
1046 1036 _set.add(ip.ip_addr)
1047 1037 except ObjectDeletedError:
1048 1038 # since we use heavy caching sometimes it happens that
1049 1039 # we get deleted objects here, we just skip them
1050 1040 pass
1051 1041
1052 1042 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1053 1043 if cache:
1054 1044 user_ips = user_ips.options(FromCache("sql_cache_short",
1055 1045 "get_user_ips_%s" % user_id))
1056 1046
1057 1047 for ip in user_ips:
1058 1048 try:
1059 1049 _set.add(ip.ip_addr)
1060 1050 except ObjectDeletedError:
1061 1051 # since we use heavy caching sometimes it happens that we get
1062 1052 # deleted objects here, we just skip them
1063 1053 pass
1064 1054 return _set or set(['0.0.0.0/0', '::/0'])
1065 1055
1066 1056
1067 1057 def set_available_permissions(config):
1068 1058 """
1069 1059 This function will propagate pylons globals with all available defined
1070 1060 permission given in db. We don't want to check each time from db for new
1071 1061 permissions since adding a new permission also requires application restart
1072 1062 ie. to decorate new views with the newly created permission
1073 1063
1074 1064 :param config: current pylons config instance
1075 1065
1076 1066 """
1077 1067 log.info('getting information about all available permissions')
1078 1068 try:
1079 1069 sa = meta.Session
1080 1070 all_perms = sa.query(Permission).all()
1081 1071 config['available_permissions'] = [x.permission_name for x in all_perms]
1082 1072 except Exception:
1083 1073 log.error(traceback.format_exc())
1084 1074 finally:
1085 1075 meta.Session.remove()
1086 1076
1087 1077
1088 1078 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1089 1079 """
1090 1080 Return the current authentication token, creating one if one doesn't
1091 1081 already exist and the save_if_missing flag is present.
1092 1082
1093 1083 :param session: pass in the pylons session, else we use the global ones
1094 1084 :param force_new: force to re-generate the token and store it in session
1095 1085 :param save_if_missing: save the newly generated token if it's missing in
1096 1086 session
1097 1087 """
1098 1088 if not session:
1099 1089 from pylons import session
1100 1090
1101 1091 if (csrf_token_key not in session and save_if_missing) or force_new:
1102 1092 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1103 1093 session[csrf_token_key] = token
1104 1094 if hasattr(session, 'save'):
1105 1095 session.save()
1106 1096 return session.get(csrf_token_key)
1107 1097
1108 1098
1109 1099 # CHECK DECORATORS
1110 1100 class CSRFRequired(object):
1111 1101 """
1112 1102 Decorator for authenticating a form
1113 1103
1114 1104 This decorator uses an authorization token stored in the client's
1115 1105 session for prevention of certain Cross-site request forgery (CSRF)
1116 1106 attacks (See
1117 1107 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1118 1108 information).
1119 1109
1120 1110 For use with the ``webhelpers.secure_form`` helper functions.
1121 1111
1122 1112 """
1123 1113 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1124 1114 except_methods=None):
1125 1115 self.token = token
1126 1116 self.header = header
1127 1117 self.except_methods = except_methods or []
1128 1118
1129 1119 def __call__(self, func):
1130 1120 return get_cython_compat_decorator(self.__wrapper, func)
1131 1121
1132 1122 def _get_csrf(self, _request):
1133 1123 return _request.POST.get(self.token, _request.headers.get(self.header))
1134 1124
1135 1125 def check_csrf(self, _request, cur_token):
1136 1126 supplied_token = self._get_csrf(_request)
1137 1127 return supplied_token and supplied_token == cur_token
1138 1128
1139 1129 def __wrapper(self, func, *fargs, **fkwargs):
1140 1130 if request.method in self.except_methods:
1141 1131 return func(*fargs, **fkwargs)
1142 1132
1143 1133 cur_token = get_csrf_token(save_if_missing=False)
1144 1134 if self.check_csrf(request, cur_token):
1145 1135 if request.POST.get(self.token):
1146 1136 del request.POST[self.token]
1147 1137 return func(*fargs, **fkwargs)
1148 1138 else:
1149 1139 reason = 'token-missing'
1150 1140 supplied_token = self._get_csrf(request)
1151 1141 if supplied_token and cur_token != supplied_token:
1152 1142 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1153 1143 supplied_token or ''[:6])
1154 1144
1155 1145 csrf_message = \
1156 1146 ("Cross-site request forgery detected, request denied. See "
1157 1147 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1158 1148 "more information.")
1159 1149 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1160 1150 'REMOTE_ADDR:%s, HEADERS:%s' % (
1161 1151 request, reason, request.remote_addr, request.headers))
1162 1152
1163 1153 raise HTTPForbidden(explanation=csrf_message)
1164 1154
1165 1155
1166 1156 class LoginRequired(object):
1167 1157 """
1168 1158 Must be logged in to execute this function else
1169 1159 redirect to login page
1170 1160
1171 1161 :param api_access: if enabled this checks only for valid auth token
1172 1162 and grants access based on valid token
1173 1163 """
1174 def __init__(self, auth_token_access=False):
1164 def __init__(self, auth_token_access=None):
1175 1165 self.auth_token_access = auth_token_access
1176 1166
1177 1167 def __call__(self, func):
1178 1168 return get_cython_compat_decorator(self.__wrapper, func)
1179 1169
1180 1170 def __wrapper(self, func, *fargs, **fkwargs):
1181 1171 from rhodecode.lib import helpers as h
1182 1172 cls = fargs[0]
1183 1173 user = cls._rhodecode_user
1184 1174 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1185 1175 log.debug('Starting login restriction checks for user: %s' % (user,))
1186 1176 # check if our IP is allowed
1187 1177 ip_access_valid = True
1188 1178 if not user.ip_allowed:
1189 1179 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1190 1180 category='warning')
1191 1181 ip_access_valid = False
1192 1182
1193 1183 # check if we used an APIKEY and it's a valid one
1194 # defined whitelist of controllers which API access will be enabled
1184 # defined white-list of controllers which API access will be enabled
1195 1185 _auth_token = request.GET.get(
1196 1186 'auth_token', '') or request.GET.get('api_key', '')
1197 1187 auth_token_access_valid = allowed_auth_token_access(
1198 1188 loc, auth_token=_auth_token)
1199 1189
1200 1190 # explicit controller is enabled or API is in our whitelist
1201 1191 if self.auth_token_access or auth_token_access_valid:
1202 1192 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1193 db_user = user.get_instance()
1203 1194
1204 if _auth_token and _auth_token in user.auth_tokens:
1195 if db_user:
1196 if self.auth_token_access:
1197 roles = self.auth_token_access
1198 else:
1199 roles = [UserApiKeys.ROLE_HTTP]
1200 token_match = db_user.authenticate_by_token(
1201 _auth_token, roles=roles, include_builtin_token=True)
1202 else:
1203 log.debug('Unable to fetch db instance for auth user: %s', user)
1204 token_match = False
1205
1206 if _auth_token and token_match:
1205 1207 auth_token_access_valid = True
1206 1208 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1207 1209 else:
1208 1210 auth_token_access_valid = False
1209 1211 if not _auth_token:
1210 1212 log.debug("AUTH TOKEN *NOT* present in request")
1211 1213 else:
1212 1214 log.warning(
1213 1215 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1214 1216
1215 1217 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1216 1218 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1217 1219 else 'AUTH_TOKEN_AUTH'
1218 1220
1219 1221 if ip_access_valid and (
1220 1222 user.is_authenticated or auth_token_access_valid):
1221 1223 log.info(
1222 1224 'user %s authenticating with:%s IS authenticated on func %s'
1223 1225 % (user, reason, loc))
1224 1226
1225 1227 # update user data to check last activity
1226 1228 user.update_lastactivity()
1227 1229 Session().commit()
1228 1230 return func(*fargs, **fkwargs)
1229 1231 else:
1230 1232 log.warning(
1231 1233 'user %s authenticating with:%s NOT authenticated on '
1232 1234 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1233 1235 % (user, reason, loc, ip_access_valid,
1234 1236 auth_token_access_valid))
1235 1237 # we preserve the get PARAM
1236 1238 came_from = request.path_qs
1237 1239
1238 1240 log.debug('redirecting to login page with %s' % (came_from,))
1239 1241 return redirect(
1240 1242 h.route_path('login', _query={'came_from': came_from}))
1241 1243
1242 1244
1243 1245 class NotAnonymous(object):
1244 1246 """
1245 1247 Must be logged in to execute this function else
1246 1248 redirect to login page"""
1247 1249
1248 1250 def __call__(self, func):
1249 1251 return get_cython_compat_decorator(self.__wrapper, func)
1250 1252
1251 1253 def __wrapper(self, func, *fargs, **fkwargs):
1252 1254 cls = fargs[0]
1253 1255 self.user = cls._rhodecode_user
1254 1256
1255 1257 log.debug('Checking if user is not anonymous @%s' % cls)
1256 1258
1257 1259 anonymous = self.user.username == User.DEFAULT_USER
1258 1260
1259 1261 if anonymous:
1260 1262 came_from = request.path_qs
1261 1263
1262 1264 import rhodecode.lib.helpers as h
1263 1265 h.flash(_('You need to be a registered user to '
1264 1266 'perform this action'),
1265 1267 category='warning')
1266 1268 return redirect(
1267 1269 h.route_path('login', _query={'came_from': came_from}))
1268 1270 else:
1269 1271 return func(*fargs, **fkwargs)
1270 1272
1271 1273
1272 1274 class XHRRequired(object):
1273 1275 def __call__(self, func):
1274 1276 return get_cython_compat_decorator(self.__wrapper, func)
1275 1277
1276 1278 def __wrapper(self, func, *fargs, **fkwargs):
1277 1279 log.debug('Checking if request is XMLHttpRequest (XHR)')
1278 1280 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1279 1281 if not request.is_xhr:
1280 1282 abort(400, detail=xhr_message)
1281 1283
1282 1284 return func(*fargs, **fkwargs)
1283 1285
1284 1286
1285 1287 class HasAcceptedRepoType(object):
1286 1288 """
1287 1289 Check if requested repo is within given repo type aliases
1288 1290
1289 1291 TODO: anderson: not sure where to put this decorator
1290 1292 """
1291 1293
1292 1294 def __init__(self, *repo_type_list):
1293 1295 self.repo_type_list = set(repo_type_list)
1294 1296
1295 1297 def __call__(self, func):
1296 1298 return get_cython_compat_decorator(self.__wrapper, func)
1297 1299
1298 1300 def __wrapper(self, func, *fargs, **fkwargs):
1299 1301 cls = fargs[0]
1300 1302 rhodecode_repo = cls.rhodecode_repo
1301 1303
1302 1304 log.debug('%s checking repo type for %s in %s',
1303 1305 self.__class__.__name__,
1304 1306 rhodecode_repo.alias, self.repo_type_list)
1305 1307
1306 1308 if rhodecode_repo.alias in self.repo_type_list:
1307 1309 return func(*fargs, **fkwargs)
1308 1310 else:
1309 1311 import rhodecode.lib.helpers as h
1310 1312 h.flash(h.literal(
1311 1313 _('Action not supported for %s.' % rhodecode_repo.alias)),
1312 1314 category='warning')
1313 1315 return redirect(
1314 1316 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1315 1317
1316 1318
1317 1319 class PermsDecorator(object):
1318 1320 """
1319 1321 Base class for controller decorators, we extract the current user from
1320 1322 the class itself, which has it stored in base controllers
1321 1323 """
1322 1324
1323 1325 def __init__(self, *required_perms):
1324 1326 self.required_perms = set(required_perms)
1325 1327
1326 1328 def __call__(self, func):
1327 1329 return get_cython_compat_decorator(self.__wrapper, func)
1328 1330
1329 1331 def __wrapper(self, func, *fargs, **fkwargs):
1330 1332 cls = fargs[0]
1331 1333 _user = cls._rhodecode_user
1332 1334
1333 1335 log.debug('checking %s permissions %s for %s %s',
1334 1336 self.__class__.__name__, self.required_perms, cls, _user)
1335 1337
1336 1338 if self.check_permissions(_user):
1337 1339 log.debug('Permission granted for %s %s', cls, _user)
1338 1340 return func(*fargs, **fkwargs)
1339 1341
1340 1342 else:
1341 1343 log.debug('Permission denied for %s %s', cls, _user)
1342 1344 anonymous = _user.username == User.DEFAULT_USER
1343 1345
1344 1346 if anonymous:
1345 1347 came_from = request.path_qs
1346 1348
1347 1349 import rhodecode.lib.helpers as h
1348 1350 h.flash(_('You need to be signed in to view this page'),
1349 1351 category='warning')
1350 1352 return redirect(
1351 1353 h.route_path('login', _query={'came_from': came_from}))
1352 1354
1353 1355 else:
1354 1356 # redirect with forbidden ret code
1355 1357 return abort(403)
1356 1358
1357 1359 def check_permissions(self, user):
1358 1360 """Dummy function for overriding"""
1359 1361 raise NotImplementedError(
1360 1362 'You have to write this function in child class')
1361 1363
1362 1364
1363 1365 class HasPermissionAllDecorator(PermsDecorator):
1364 1366 """
1365 1367 Checks for access permission for all given predicates. All of them
1366 1368 have to be meet in order to fulfill the request
1367 1369 """
1368 1370
1369 1371 def check_permissions(self, user):
1370 1372 perms = user.permissions_with_scope({})
1371 1373 if self.required_perms.issubset(perms['global']):
1372 1374 return True
1373 1375 return False
1374 1376
1375 1377
1376 1378 class HasPermissionAnyDecorator(PermsDecorator):
1377 1379 """
1378 1380 Checks for access permission for any of given predicates. In order to
1379 1381 fulfill the request any of predicates must be meet
1380 1382 """
1381 1383
1382 1384 def check_permissions(self, user):
1383 1385 perms = user.permissions_with_scope({})
1384 1386 if self.required_perms.intersection(perms['global']):
1385 1387 return True
1386 1388 return False
1387 1389
1388 1390
1389 1391 class HasRepoPermissionAllDecorator(PermsDecorator):
1390 1392 """
1391 1393 Checks for access permission for all given predicates for specific
1392 1394 repository. All of them have to be meet in order to fulfill the request
1393 1395 """
1394 1396
1395 1397 def check_permissions(self, user):
1396 1398 perms = user.permissions
1397 1399 repo_name = get_repo_slug(request)
1398 1400 try:
1399 1401 user_perms = set([perms['repositories'][repo_name]])
1400 1402 except KeyError:
1401 1403 return False
1402 1404 if self.required_perms.issubset(user_perms):
1403 1405 return True
1404 1406 return False
1405 1407
1406 1408
1407 1409 class HasRepoPermissionAnyDecorator(PermsDecorator):
1408 1410 """
1409 1411 Checks for access permission for any of given predicates for specific
1410 1412 repository. In order to fulfill the request any of predicates must be meet
1411 1413 """
1412 1414
1413 1415 def check_permissions(self, user):
1414 1416 perms = user.permissions
1415 1417 repo_name = get_repo_slug(request)
1416 1418 try:
1417 1419 user_perms = set([perms['repositories'][repo_name]])
1418 1420 except KeyError:
1419 1421 return False
1420 1422
1421 1423 if self.required_perms.intersection(user_perms):
1422 1424 return True
1423 1425 return False
1424 1426
1425 1427
1426 1428 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1427 1429 """
1428 1430 Checks for access permission for all given predicates for specific
1429 1431 repository group. All of them have to be meet in order to
1430 1432 fulfill the request
1431 1433 """
1432 1434
1433 1435 def check_permissions(self, user):
1434 1436 perms = user.permissions
1435 1437 group_name = get_repo_group_slug(request)
1436 1438 try:
1437 1439 user_perms = set([perms['repositories_groups'][group_name]])
1438 1440 except KeyError:
1439 1441 return False
1440 1442
1441 1443 if self.required_perms.issubset(user_perms):
1442 1444 return True
1443 1445 return False
1444 1446
1445 1447
1446 1448 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1447 1449 """
1448 1450 Checks for access permission for any of given predicates for specific
1449 1451 repository group. In order to fulfill the request any
1450 1452 of predicates must be met
1451 1453 """
1452 1454
1453 1455 def check_permissions(self, user):
1454 1456 perms = user.permissions
1455 1457 group_name = get_repo_group_slug(request)
1456 1458 try:
1457 1459 user_perms = set([perms['repositories_groups'][group_name]])
1458 1460 except KeyError:
1459 1461 return False
1460 1462
1461 1463 if self.required_perms.intersection(user_perms):
1462 1464 return True
1463 1465 return False
1464 1466
1465 1467
1466 1468 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1467 1469 """
1468 1470 Checks for access permission for all given predicates for specific
1469 1471 user group. All of them have to be meet in order to fulfill the request
1470 1472 """
1471 1473
1472 1474 def check_permissions(self, user):
1473 1475 perms = user.permissions
1474 1476 group_name = get_user_group_slug(request)
1475 1477 try:
1476 1478 user_perms = set([perms['user_groups'][group_name]])
1477 1479 except KeyError:
1478 1480 return False
1479 1481
1480 1482 if self.required_perms.issubset(user_perms):
1481 1483 return True
1482 1484 return False
1483 1485
1484 1486
1485 1487 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1486 1488 """
1487 1489 Checks for access permission for any of given predicates for specific
1488 1490 user group. In order to fulfill the request any of predicates must be meet
1489 1491 """
1490 1492
1491 1493 def check_permissions(self, user):
1492 1494 perms = user.permissions
1493 1495 group_name = get_user_group_slug(request)
1494 1496 try:
1495 1497 user_perms = set([perms['user_groups'][group_name]])
1496 1498 except KeyError:
1497 1499 return False
1498 1500
1499 1501 if self.required_perms.intersection(user_perms):
1500 1502 return True
1501 1503 return False
1502 1504
1503 1505
1504 1506 # CHECK FUNCTIONS
1505 1507 class PermsFunction(object):
1506 1508 """Base function for other check functions"""
1507 1509
1508 1510 def __init__(self, *perms):
1509 1511 self.required_perms = set(perms)
1510 1512 self.repo_name = None
1511 1513 self.repo_group_name = None
1512 1514 self.user_group_name = None
1513 1515
1514 1516 def __bool__(self):
1515 1517 frame = inspect.currentframe()
1516 1518 stack_trace = traceback.format_stack(frame)
1517 1519 log.error('Checking bool value on a class instance of perm '
1518 1520 'function is not allowed: %s' % ''.join(stack_trace))
1519 1521 # rather than throwing errors, here we always return False so if by
1520 1522 # accident someone checks truth for just an instance it will always end
1521 1523 # up in returning False
1522 1524 return False
1523 1525 __nonzero__ = __bool__
1524 1526
1525 1527 def __call__(self, check_location='', user=None):
1526 1528 if not user:
1527 1529 log.debug('Using user attribute from global request')
1528 1530 # TODO: remove this someday,put as user as attribute here
1529 1531 user = request.user
1530 1532
1531 1533 # init auth user if not already given
1532 1534 if not isinstance(user, AuthUser):
1533 1535 log.debug('Wrapping user %s into AuthUser', user)
1534 1536 user = AuthUser(user.user_id)
1535 1537
1536 1538 cls_name = self.__class__.__name__
1537 1539 check_scope = self._get_check_scope(cls_name)
1538 1540 check_location = check_location or 'unspecified location'
1539 1541
1540 1542 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1541 1543 self.required_perms, user, check_scope, check_location)
1542 1544 if not user:
1543 1545 log.warning('Empty user given for permission check')
1544 1546 return False
1545 1547
1546 1548 if self.check_permissions(user):
1547 1549 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1548 1550 check_scope, user, check_location)
1549 1551 return True
1550 1552
1551 1553 else:
1552 1554 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1553 1555 check_scope, user, check_location)
1554 1556 return False
1555 1557
1556 1558 def _get_check_scope(self, cls_name):
1557 1559 return {
1558 1560 'HasPermissionAll': 'GLOBAL',
1559 1561 'HasPermissionAny': 'GLOBAL',
1560 1562 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1561 1563 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1562 1564 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1563 1565 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1564 1566 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1565 1567 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1566 1568 }.get(cls_name, '?:%s' % cls_name)
1567 1569
1568 1570 def check_permissions(self, user):
1569 1571 """Dummy function for overriding"""
1570 1572 raise Exception('You have to write this function in child class')
1571 1573
1572 1574
1573 1575 class HasPermissionAll(PermsFunction):
1574 1576 def check_permissions(self, user):
1575 1577 perms = user.permissions_with_scope({})
1576 1578 if self.required_perms.issubset(perms.get('global')):
1577 1579 return True
1578 1580 return False
1579 1581
1580 1582
1581 1583 class HasPermissionAny(PermsFunction):
1582 1584 def check_permissions(self, user):
1583 1585 perms = user.permissions_with_scope({})
1584 1586 if self.required_perms.intersection(perms.get('global')):
1585 1587 return True
1586 1588 return False
1587 1589
1588 1590
1589 1591 class HasRepoPermissionAll(PermsFunction):
1590 1592 def __call__(self, repo_name=None, check_location='', user=None):
1591 1593 self.repo_name = repo_name
1592 1594 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1593 1595
1594 1596 def check_permissions(self, user):
1595 1597 if not self.repo_name:
1596 1598 self.repo_name = get_repo_slug(request)
1597 1599
1598 1600 perms = user.permissions
1599 1601 try:
1600 1602 user_perms = set([perms['repositories'][self.repo_name]])
1601 1603 except KeyError:
1602 1604 return False
1603 1605 if self.required_perms.issubset(user_perms):
1604 1606 return True
1605 1607 return False
1606 1608
1607 1609
1608 1610 class HasRepoPermissionAny(PermsFunction):
1609 1611 def __call__(self, repo_name=None, check_location='', user=None):
1610 1612 self.repo_name = repo_name
1611 1613 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1612 1614
1613 1615 def check_permissions(self, user):
1614 1616 if not self.repo_name:
1615 1617 self.repo_name = get_repo_slug(request)
1616 1618
1617 1619 perms = user.permissions
1618 1620 try:
1619 1621 user_perms = set([perms['repositories'][self.repo_name]])
1620 1622 except KeyError:
1621 1623 return False
1622 1624 if self.required_perms.intersection(user_perms):
1623 1625 return True
1624 1626 return False
1625 1627
1626 1628
1627 1629 class HasRepoGroupPermissionAny(PermsFunction):
1628 1630 def __call__(self, group_name=None, check_location='', user=None):
1629 1631 self.repo_group_name = group_name
1630 1632 return super(HasRepoGroupPermissionAny, self).__call__(
1631 1633 check_location, user)
1632 1634
1633 1635 def check_permissions(self, user):
1634 1636 perms = user.permissions
1635 1637 try:
1636 1638 user_perms = set(
1637 1639 [perms['repositories_groups'][self.repo_group_name]])
1638 1640 except KeyError:
1639 1641 return False
1640 1642 if self.required_perms.intersection(user_perms):
1641 1643 return True
1642 1644 return False
1643 1645
1644 1646
1645 1647 class HasRepoGroupPermissionAll(PermsFunction):
1646 1648 def __call__(self, group_name=None, check_location='', user=None):
1647 1649 self.repo_group_name = group_name
1648 1650 return super(HasRepoGroupPermissionAll, self).__call__(
1649 1651 check_location, user)
1650 1652
1651 1653 def check_permissions(self, user):
1652 1654 perms = user.permissions
1653 1655 try:
1654 1656 user_perms = set(
1655 1657 [perms['repositories_groups'][self.repo_group_name]])
1656 1658 except KeyError:
1657 1659 return False
1658 1660 if self.required_perms.issubset(user_perms):
1659 1661 return True
1660 1662 return False
1661 1663
1662 1664
1663 1665 class HasUserGroupPermissionAny(PermsFunction):
1664 1666 def __call__(self, user_group_name=None, check_location='', user=None):
1665 1667 self.user_group_name = user_group_name
1666 1668 return super(HasUserGroupPermissionAny, self).__call__(
1667 1669 check_location, user)
1668 1670
1669 1671 def check_permissions(self, user):
1670 1672 perms = user.permissions
1671 1673 try:
1672 1674 user_perms = set([perms['user_groups'][self.user_group_name]])
1673 1675 except KeyError:
1674 1676 return False
1675 1677 if self.required_perms.intersection(user_perms):
1676 1678 return True
1677 1679 return False
1678 1680
1679 1681
1680 1682 class HasUserGroupPermissionAll(PermsFunction):
1681 1683 def __call__(self, user_group_name=None, check_location='', user=None):
1682 1684 self.user_group_name = user_group_name
1683 1685 return super(HasUserGroupPermissionAll, self).__call__(
1684 1686 check_location, user)
1685 1687
1686 1688 def check_permissions(self, user):
1687 1689 perms = user.permissions
1688 1690 try:
1689 1691 user_perms = set([perms['user_groups'][self.user_group_name]])
1690 1692 except KeyError:
1691 1693 return False
1692 1694 if self.required_perms.issubset(user_perms):
1693 1695 return True
1694 1696 return False
1695 1697
1696 1698
1697 1699 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1698 1700 class HasPermissionAnyMiddleware(object):
1699 1701 def __init__(self, *perms):
1700 1702 self.required_perms = set(perms)
1701 1703
1702 1704 def __call__(self, user, repo_name):
1703 1705 # repo_name MUST be unicode, since we handle keys in permission
1704 1706 # dict by unicode
1705 1707 repo_name = safe_unicode(repo_name)
1706 1708 user = AuthUser(user.user_id)
1707 1709 log.debug(
1708 1710 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1709 1711 self.required_perms, user, repo_name)
1710 1712
1711 1713 if self.check_permissions(user, repo_name):
1712 1714 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1713 1715 repo_name, user, 'PermissionMiddleware')
1714 1716 return True
1715 1717
1716 1718 else:
1717 1719 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1718 1720 repo_name, user, 'PermissionMiddleware')
1719 1721 return False
1720 1722
1721 1723 def check_permissions(self, user, repo_name):
1722 1724 perms = user.permissions_with_scope({'repo_name': repo_name})
1723 1725
1724 1726 try:
1725 1727 user_perms = set([perms['repositories'][repo_name]])
1726 1728 except Exception:
1727 1729 log.exception('Error while accessing user permissions')
1728 1730 return False
1729 1731
1730 1732 if self.required_perms.intersection(user_perms):
1731 1733 return True
1732 1734 return False
1733 1735
1734 1736
1735 1737 # SPECIAL VERSION TO HANDLE API AUTH
1736 1738 class _BaseApiPerm(object):
1737 1739 def __init__(self, *perms):
1738 1740 self.required_perms = set(perms)
1739 1741
1740 1742 def __call__(self, check_location=None, user=None, repo_name=None,
1741 1743 group_name=None, user_group_name=None):
1742 1744 cls_name = self.__class__.__name__
1743 1745 check_scope = 'global:%s' % (self.required_perms,)
1744 1746 if repo_name:
1745 1747 check_scope += ', repo_name:%s' % (repo_name,)
1746 1748
1747 1749 if group_name:
1748 1750 check_scope += ', repo_group_name:%s' % (group_name,)
1749 1751
1750 1752 if user_group_name:
1751 1753 check_scope += ', user_group_name:%s' % (user_group_name,)
1752 1754
1753 1755 log.debug(
1754 1756 'checking cls:%s %s %s @ %s'
1755 1757 % (cls_name, self.required_perms, check_scope, check_location))
1756 1758 if not user:
1757 1759 log.debug('Empty User passed into arguments')
1758 1760 return False
1759 1761
1760 1762 # process user
1761 1763 if not isinstance(user, AuthUser):
1762 1764 user = AuthUser(user.user_id)
1763 1765 if not check_location:
1764 1766 check_location = 'unspecified'
1765 1767 if self.check_permissions(user.permissions, repo_name, group_name,
1766 1768 user_group_name):
1767 1769 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1768 1770 check_scope, user, check_location)
1769 1771 return True
1770 1772
1771 1773 else:
1772 1774 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1773 1775 check_scope, user, check_location)
1774 1776 return False
1775 1777
1776 1778 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1777 1779 user_group_name=None):
1778 1780 """
1779 1781 implement in child class should return True if permissions are ok,
1780 1782 False otherwise
1781 1783
1782 1784 :param perm_defs: dict with permission definitions
1783 1785 :param repo_name: repo name
1784 1786 """
1785 1787 raise NotImplementedError()
1786 1788
1787 1789
1788 1790 class HasPermissionAllApi(_BaseApiPerm):
1789 1791 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1790 1792 user_group_name=None):
1791 1793 if self.required_perms.issubset(perm_defs.get('global')):
1792 1794 return True
1793 1795 return False
1794 1796
1795 1797
1796 1798 class HasPermissionAnyApi(_BaseApiPerm):
1797 1799 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1798 1800 user_group_name=None):
1799 1801 if self.required_perms.intersection(perm_defs.get('global')):
1800 1802 return True
1801 1803 return False
1802 1804
1803 1805
1804 1806 class HasRepoPermissionAllApi(_BaseApiPerm):
1805 1807 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1806 1808 user_group_name=None):
1807 1809 try:
1808 1810 _user_perms = set([perm_defs['repositories'][repo_name]])
1809 1811 except KeyError:
1810 1812 log.warning(traceback.format_exc())
1811 1813 return False
1812 1814 if self.required_perms.issubset(_user_perms):
1813 1815 return True
1814 1816 return False
1815 1817
1816 1818
1817 1819 class HasRepoPermissionAnyApi(_BaseApiPerm):
1818 1820 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1819 1821 user_group_name=None):
1820 1822 try:
1821 1823 _user_perms = set([perm_defs['repositories'][repo_name]])
1822 1824 except KeyError:
1823 1825 log.warning(traceback.format_exc())
1824 1826 return False
1825 1827 if self.required_perms.intersection(_user_perms):
1826 1828 return True
1827 1829 return False
1828 1830
1829 1831
1830 1832 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1831 1833 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1832 1834 user_group_name=None):
1833 1835 try:
1834 1836 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1835 1837 except KeyError:
1836 1838 log.warning(traceback.format_exc())
1837 1839 return False
1838 1840 if self.required_perms.intersection(_user_perms):
1839 1841 return True
1840 1842 return False
1841 1843
1842 1844
1843 1845 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1844 1846 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1845 1847 user_group_name=None):
1846 1848 try:
1847 1849 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1848 1850 except KeyError:
1849 1851 log.warning(traceback.format_exc())
1850 1852 return False
1851 1853 if self.required_perms.issubset(_user_perms):
1852 1854 return True
1853 1855 return False
1854 1856
1855 1857
1856 1858 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1857 1859 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1858 1860 user_group_name=None):
1859 1861 try:
1860 1862 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1861 1863 except KeyError:
1862 1864 log.warning(traceback.format_exc())
1863 1865 return False
1864 1866 if self.required_perms.intersection(_user_perms):
1865 1867 return True
1866 1868 return False
1867 1869
1868 1870
1869 1871 def check_ip_access(source_ip, allowed_ips=None):
1870 1872 """
1871 1873 Checks if source_ip is a subnet of any of allowed_ips.
1872 1874
1873 1875 :param source_ip:
1874 1876 :param allowed_ips: list of allowed ips together with mask
1875 1877 """
1876 1878 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1877 1879 source_ip_address = ipaddress.ip_address(source_ip)
1878 1880 if isinstance(allowed_ips, (tuple, list, set)):
1879 1881 for ip in allowed_ips:
1880 1882 try:
1881 1883 network_address = ipaddress.ip_network(ip, strict=False)
1882 1884 if source_ip_address in network_address:
1883 1885 log.debug('IP %s is network %s' %
1884 1886 (source_ip_address, network_address))
1885 1887 return True
1886 1888 # for any case we cannot determine the IP, don't crash just
1887 1889 # skip it and log as error, we want to say forbidden still when
1888 1890 # sending bad IP
1889 1891 except Exception:
1890 1892 log.error(traceback.format_exc())
1891 1893 continue
1892 1894 return False
1893 1895
1894 1896
1895 1897 def get_cython_compat_decorator(wrapper, func):
1896 1898 """
1897 1899 Creates a cython compatible decorator. The previously used
1898 1900 decorator.decorator() function seems to be incompatible with cython.
1899 1901
1900 1902 :param wrapper: __wrapper method of the decorator class
1901 1903 :param func: decorated function
1902 1904 """
1903 1905 @wraps(func)
1904 1906 def local_wrapper(*args, **kwds):
1905 1907 return wrapper(func, *args, **kwds)
1906 1908 local_wrapper.__wrapped__ = func
1907 1909 return local_wrapper
@@ -1,3861 +1,3908 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from webob.exc import HTTPNotFound
46 46 from zope.cachedescriptors.property import Lazy as LazyProperty
47 47
48 48 from pylons import url
49 49 from pylons.i18n.translation import lazy_ugettext as _
50 50
51 51 from rhodecode.lib.vcs import get_vcs_instance
52 52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 53 from rhodecode.lib.utils2 import (
54 54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 56 glob2re, StrictAttributeDict)
57 57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 58 from rhodecode.lib.ext_json import json
59 59 from rhodecode.lib.caching_query import FromCache
60 60 from rhodecode.lib.encrypt import AESCipher
61 61
62 62 from rhodecode.model.meta import Base, Session
63 63
64 64 URL_SEP = '/'
65 65 log = logging.getLogger(__name__)
66 66
67 67 # =============================================================================
68 68 # BASE CLASSES
69 69 # =============================================================================
70 70
71 71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 72 # beaker.session.secret if first is not set.
73 73 # and initialized at environment.py
74 74 ENCRYPTION_KEY = None
75 75
76 76 # used to sort permissions by types, '#' used here is not allowed to be in
77 77 # usernames, and it's very early in sorted string.printable table.
78 78 PERMISSION_TYPE_SORT = {
79 79 'admin': '####',
80 80 'write': '###',
81 81 'read': '##',
82 82 'none': '#',
83 83 }
84 84
85 85
86 86 def display_sort(obj):
87 87 """
88 88 Sort function used to sort permissions in .permissions() function of
89 89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 90 of all other resources
91 91 """
92 92
93 93 if obj.username == User.DEFAULT_USER:
94 94 return '#####'
95 95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 96 return prefix + obj.username
97 97
98 98
99 99 def _hash_key(k):
100 100 return md5_safe(k)
101 101
102 102
103 103 class EncryptedTextValue(TypeDecorator):
104 104 """
105 105 Special column for encrypted long text data, use like::
106 106
107 107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108 108
109 109 This column is intelligent so if value is in unencrypted form it return
110 110 unencrypted form, but on save it always encrypts
111 111 """
112 112 impl = Text
113 113
114 114 def process_bind_param(self, value, dialect):
115 115 if not value:
116 116 return value
117 117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 118 # protect against double encrypting if someone manually starts
119 119 # doing
120 120 raise ValueError('value needs to be in unencrypted format, ie. '
121 121 'not starting with enc$aes')
122 122 return 'enc$aes_hmac$%s' % AESCipher(
123 123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124 124
125 125 def process_result_value(self, value, dialect):
126 126 import rhodecode
127 127
128 128 if not value:
129 129 return value
130 130
131 131 parts = value.split('$', 3)
132 132 if not len(parts) == 3:
133 133 # probably not encrypted values
134 134 return value
135 135 else:
136 136 if parts[0] != 'enc':
137 137 # parts ok but without our header ?
138 138 return value
139 139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 140 'rhodecode.encrypted_values.strict') or True)
141 141 # at that stage we know it's our encryption
142 142 if parts[1] == 'aes':
143 143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 144 elif parts[1] == 'aes_hmac':
145 145 decrypted_data = AESCipher(
146 146 ENCRYPTION_KEY, hmac=True,
147 147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 148 else:
149 149 raise ValueError(
150 150 'Encryption type part is wrong, must be `aes` '
151 151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 152 return decrypted_data
153 153
154 154
155 155 class BaseModel(object):
156 156 """
157 157 Base Model for all classes
158 158 """
159 159
160 160 @classmethod
161 161 def _get_keys(cls):
162 162 """return column names for this model """
163 163 return class_mapper(cls).c.keys()
164 164
165 165 def get_dict(self):
166 166 """
167 167 return dict with keys and values corresponding
168 168 to this model data """
169 169
170 170 d = {}
171 171 for k in self._get_keys():
172 172 d[k] = getattr(self, k)
173 173
174 174 # also use __json__() if present to get additional fields
175 175 _json_attr = getattr(self, '__json__', None)
176 176 if _json_attr:
177 177 # update with attributes from __json__
178 178 if callable(_json_attr):
179 179 _json_attr = _json_attr()
180 180 for k, val in _json_attr.iteritems():
181 181 d[k] = val
182 182 return d
183 183
184 184 def get_appstruct(self):
185 185 """return list with keys and values tuples corresponding
186 186 to this model data """
187 187
188 188 l = []
189 189 for k in self._get_keys():
190 190 l.append((k, getattr(self, k),))
191 191 return l
192 192
193 193 def populate_obj(self, populate_dict):
194 194 """populate model with data from given populate_dict"""
195 195
196 196 for k in self._get_keys():
197 197 if k in populate_dict:
198 198 setattr(self, k, populate_dict[k])
199 199
200 200 @classmethod
201 201 def query(cls):
202 202 return Session().query(cls)
203 203
204 204 @classmethod
205 205 def get(cls, id_):
206 206 if id_:
207 207 return cls.query().get(id_)
208 208
209 209 @classmethod
210 210 def get_or_404(cls, id_):
211 211 try:
212 212 id_ = int(id_)
213 213 except (TypeError, ValueError):
214 214 raise HTTPNotFound
215 215
216 216 res = cls.query().get(id_)
217 217 if not res:
218 218 raise HTTPNotFound
219 219 return res
220 220
221 221 @classmethod
222 222 def getAll(cls):
223 223 # deprecated and left for backward compatibility
224 224 return cls.get_all()
225 225
226 226 @classmethod
227 227 def get_all(cls):
228 228 return cls.query().all()
229 229
230 230 @classmethod
231 231 def delete(cls, id_):
232 232 obj = cls.query().get(id_)
233 233 Session().delete(obj)
234 234
235 235 @classmethod
236 236 def identity_cache(cls, session, attr_name, value):
237 237 exist_in_session = []
238 238 for (item_cls, pkey), instance in session.identity_map.items():
239 239 if cls == item_cls and getattr(instance, attr_name) == value:
240 240 exist_in_session.append(instance)
241 241 if exist_in_session:
242 242 if len(exist_in_session) == 1:
243 243 return exist_in_session[0]
244 244 log.exception(
245 245 'multiple objects with attr %s and '
246 246 'value %s found with same name: %r',
247 247 attr_name, value, exist_in_session)
248 248
249 249 def __repr__(self):
250 250 if hasattr(self, '__unicode__'):
251 251 # python repr needs to return str
252 252 try:
253 253 return safe_str(self.__unicode__())
254 254 except UnicodeDecodeError:
255 255 pass
256 256 return '<DB:%s>' % (self.__class__.__name__)
257 257
258 258
259 259 class RhodeCodeSetting(Base, BaseModel):
260 260 __tablename__ = 'rhodecode_settings'
261 261 __table_args__ = (
262 262 UniqueConstraint('app_settings_name'),
263 263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 265 )
266 266
267 267 SETTINGS_TYPES = {
268 268 'str': safe_str,
269 269 'int': safe_int,
270 270 'unicode': safe_unicode,
271 271 'bool': str2bool,
272 272 'list': functools.partial(aslist, sep=',')
273 273 }
274 274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 275 GLOBAL_CONF_KEY = 'app_settings'
276 276
277 277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281 281
282 282 def __init__(self, key='', val='', type='unicode'):
283 283 self.app_settings_name = key
284 284 self.app_settings_type = type
285 285 self.app_settings_value = val
286 286
287 287 @validates('_app_settings_value')
288 288 def validate_settings_value(self, key, val):
289 289 assert type(val) == unicode
290 290 return val
291 291
292 292 @hybrid_property
293 293 def app_settings_value(self):
294 294 v = self._app_settings_value
295 295 _type = self.app_settings_type
296 296 if _type:
297 297 _type = self.app_settings_type.split('.')[0]
298 298 # decode the encrypted value
299 299 if 'encrypted' in self.app_settings_type:
300 300 cipher = EncryptedTextValue()
301 301 v = safe_unicode(cipher.process_result_value(v, None))
302 302
303 303 converter = self.SETTINGS_TYPES.get(_type) or \
304 304 self.SETTINGS_TYPES['unicode']
305 305 return converter(v)
306 306
307 307 @app_settings_value.setter
308 308 def app_settings_value(self, val):
309 309 """
310 310 Setter that will always make sure we use unicode in app_settings_value
311 311
312 312 :param val:
313 313 """
314 314 val = safe_unicode(val)
315 315 # encode the encrypted value
316 316 if 'encrypted' in self.app_settings_type:
317 317 cipher = EncryptedTextValue()
318 318 val = safe_unicode(cipher.process_bind_param(val, None))
319 319 self._app_settings_value = val
320 320
321 321 @hybrid_property
322 322 def app_settings_type(self):
323 323 return self._app_settings_type
324 324
325 325 @app_settings_type.setter
326 326 def app_settings_type(self, val):
327 327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 328 raise Exception('type must be one of %s got %s'
329 329 % (self.SETTINGS_TYPES.keys(), val))
330 330 self._app_settings_type = val
331 331
332 332 def __unicode__(self):
333 333 return u"<%s('%s:%s[%s]')>" % (
334 334 self.__class__.__name__,
335 335 self.app_settings_name, self.app_settings_value,
336 336 self.app_settings_type
337 337 )
338 338
339 339
340 340 class RhodeCodeUi(Base, BaseModel):
341 341 __tablename__ = 'rhodecode_ui'
342 342 __table_args__ = (
343 343 UniqueConstraint('ui_key'),
344 344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 346 )
347 347
348 348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 349 # HG
350 350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 351 HOOK_PULL = 'outgoing.pull_logger'
352 352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 353 HOOK_PUSH = 'changegroup.push_logger'
354 354
355 355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 356 # git part is currently hardcoded.
357 357
358 358 # SVN PATTERNS
359 359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 360 SVN_TAG_ID = 'vcs_svn_tag'
361 361
362 362 ui_id = Column(
363 363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 364 primary_key=True)
365 365 ui_section = Column(
366 366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 367 ui_key = Column(
368 368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 369 ui_value = Column(
370 370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 371 ui_active = Column(
372 372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373 373
374 374 def __repr__(self):
375 375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 376 self.ui_key, self.ui_value)
377 377
378 378
379 379 class RepoRhodeCodeSetting(Base, BaseModel):
380 380 __tablename__ = 'repo_rhodecode_settings'
381 381 __table_args__ = (
382 382 UniqueConstraint(
383 383 'app_settings_name', 'repository_id',
384 384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 387 )
388 388
389 389 repository_id = Column(
390 390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 391 nullable=False)
392 392 app_settings_id = Column(
393 393 "app_settings_id", Integer(), nullable=False, unique=True,
394 394 default=None, primary_key=True)
395 395 app_settings_name = Column(
396 396 "app_settings_name", String(255), nullable=True, unique=None,
397 397 default=None)
398 398 _app_settings_value = Column(
399 399 "app_settings_value", String(4096), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_type = Column(
402 402 "app_settings_type", String(255), nullable=True, unique=None,
403 403 default=None)
404 404
405 405 repository = relationship('Repository')
406 406
407 407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 408 self.repository_id = repository_id
409 409 self.app_settings_name = key
410 410 self.app_settings_type = type
411 411 self.app_settings_value = val
412 412
413 413 @validates('_app_settings_value')
414 414 def validate_settings_value(self, key, val):
415 415 assert type(val) == unicode
416 416 return val
417 417
418 418 @hybrid_property
419 419 def app_settings_value(self):
420 420 v = self._app_settings_value
421 421 type_ = self.app_settings_type
422 422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 424 return converter(v)
425 425
426 426 @app_settings_value.setter
427 427 def app_settings_value(self, val):
428 428 """
429 429 Setter that will always make sure we use unicode in app_settings_value
430 430
431 431 :param val:
432 432 """
433 433 self._app_settings_value = safe_unicode(val)
434 434
435 435 @hybrid_property
436 436 def app_settings_type(self):
437 437 return self._app_settings_type
438 438
439 439 @app_settings_type.setter
440 440 def app_settings_type(self, val):
441 441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 442 if val not in SETTINGS_TYPES:
443 443 raise Exception('type must be one of %s got %s'
444 444 % (SETTINGS_TYPES.keys(), val))
445 445 self._app_settings_type = val
446 446
447 447 def __unicode__(self):
448 448 return u"<%s('%s:%s:%s[%s]')>" % (
449 449 self.__class__.__name__, self.repository.repo_name,
450 450 self.app_settings_name, self.app_settings_value,
451 451 self.app_settings_type
452 452 )
453 453
454 454
455 455 class RepoRhodeCodeUi(Base, BaseModel):
456 456 __tablename__ = 'repo_rhodecode_ui'
457 457 __table_args__ = (
458 458 UniqueConstraint(
459 459 'repository_id', 'ui_section', 'ui_key',
460 460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
463 463 )
464 464
465 465 repository_id = Column(
466 466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 467 nullable=False)
468 468 ui_id = Column(
469 469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 470 primary_key=True)
471 471 ui_section = Column(
472 472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 473 ui_key = Column(
474 474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 475 ui_value = Column(
476 476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 477 ui_active = Column(
478 478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479 479
480 480 repository = relationship('Repository')
481 481
482 482 def __repr__(self):
483 483 return '<%s[%s:%s]%s=>%s]>' % (
484 484 self.__class__.__name__, self.repository.repo_name,
485 485 self.ui_section, self.ui_key, self.ui_value)
486 486
487 487
488 488 class User(Base, BaseModel):
489 489 __tablename__ = 'users'
490 490 __table_args__ = (
491 491 UniqueConstraint('username'), UniqueConstraint('email'),
492 492 Index('u_username_idx', 'username'),
493 493 Index('u_email_idx', 'email'),
494 494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 496 )
497 497 DEFAULT_USER = 'default'
498 498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500 500
501 501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516 516
517 517 user_log = relationship('UserLog')
518 518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519 519
520 520 repositories = relationship('Repository')
521 521 repository_groups = relationship('RepoGroup')
522 522 user_groups = relationship('UserGroup')
523 523
524 524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526 526
527 527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530 530
531 531 group_member = relationship('UserGroupMember', cascade='all')
532 532
533 533 notifications = relationship('UserNotification', cascade='all')
534 534 # notifications assigned to this user
535 535 user_created_notifications = relationship('Notification', cascade='all')
536 536 # comments created by this user
537 537 user_comments = relationship('ChangesetComment', cascade='all')
538 538 # user profile extra info
539 539 user_emails = relationship('UserEmailMap', cascade='all')
540 540 user_ip_map = relationship('UserIpMap', cascade='all')
541 541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 542 # gists
543 543 user_gists = relationship('Gist', cascade='all')
544 544 # user pull requests
545 545 user_pull_requests = relationship('PullRequest', cascade='all')
546 546 # external identities
547 547 extenal_identities = relationship(
548 548 'ExternalIdentity',
549 549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 550 cascade='all')
551 551
552 552 def __unicode__(self):
553 553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 554 self.user_id, self.username)
555 555
556 556 @hybrid_property
557 557 def email(self):
558 558 return self._email
559 559
560 560 @email.setter
561 561 def email(self, val):
562 562 self._email = val.lower() if val else None
563 563
564 564 @property
565 565 def firstname(self):
566 566 # alias for future
567 567 return self.name
568 568
569 569 @property
570 570 def emails(self):
571 571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 572 return [self.email] + [x.email for x in other]
573 573
574 574 @property
575 575 def auth_tokens(self):
576 576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577 577
578 578 @property
579 579 def extra_auth_tokens(self):
580 580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581 581
582 582 @property
583 583 def feed_token(self):
584 return self.get_feed_token()
585
586 def get_feed_token(self):
584 587 feed_tokens = UserApiKeys.query()\
585 588 .filter(UserApiKeys.user == self)\
586 589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 590 .all()
588 591 if feed_tokens:
589 592 return feed_tokens[0].api_key
590 else:
591 # use the main token so we don't end up with nothing...
592 return self.api_key
593 return 'NO_FEED_TOKEN_AVAILABLE'
593 594
594 595 @classmethod
595 596 def extra_valid_auth_tokens(cls, user, role=None):
596 597 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 598 .filter(or_(UserApiKeys.expires == -1,
598 599 UserApiKeys.expires >= time.time()))
599 600 if role:
600 601 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 602 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 603 return tokens.all()
603 604
605 def authenticate_by_token(self, auth_token, roles=None,
606 include_builtin_token=False):
607 from rhodecode.lib import auth
608
609 log.debug('Trying to authenticate user: %s via auth-token, '
610 'and roles: %s', self, roles)
611
612 if not auth_token:
613 return False
614
615 crypto_backend = auth.crypto_backend()
616
617 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
618 tokens_q = UserApiKeys.query()\
619 .filter(UserApiKeys.user_id == self.user_id)\
620 .filter(or_(UserApiKeys.expires == -1,
621 UserApiKeys.expires >= time.time()))
622
623 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
624
625 maybe_builtin = []
626 if include_builtin_token:
627 maybe_builtin = [AttributeDict({'api_key': self.api_key})]
628
629 plain_tokens = []
630 hash_tokens = []
631
632 for token in tokens_q.all() + maybe_builtin:
633 if token.api_key.startswith(crypto_backend.ENC_PREF):
634 hash_tokens.append(token.api_key)
635 else:
636 plain_tokens.append(token.api_key)
637
638 is_plain_match = auth_token in plain_tokens
639 if is_plain_match:
640 return True
641
642 for hashed in hash_tokens:
643 # marcink: this is expensive to calculate, but the most secure
644 match = crypto_backend.hash_check(auth_token, hashed)
645 if match:
646 return True
647
648 return False
649
604 650 @property
605 651 def builtin_token_roles(self):
606 return map(UserApiKeys._get_role_name, [
652 roles = [
607 653 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 ])
654 ]
655 return map(UserApiKeys._get_role_name, roles)
609 656
610 657 @property
611 658 def ip_addresses(self):
612 659 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 660 return [x.ip_addr for x in ret]
614 661
615 662 @property
616 663 def username_and_name(self):
617 664 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618 665
619 666 @property
620 667 def username_or_name_or_email(self):
621 668 full_name = self.full_name if self.full_name is not ' ' else None
622 669 return self.username or full_name or self.email
623 670
624 671 @property
625 672 def full_name(self):
626 673 return '%s %s' % (self.firstname, self.lastname)
627 674
628 675 @property
629 676 def full_name_or_username(self):
630 677 return ('%s %s' % (self.firstname, self.lastname)
631 678 if (self.firstname and self.lastname) else self.username)
632 679
633 680 @property
634 681 def full_contact(self):
635 682 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636 683
637 684 @property
638 685 def short_contact(self):
639 686 return '%s %s' % (self.firstname, self.lastname)
640 687
641 688 @property
642 689 def is_admin(self):
643 690 return self.admin
644 691
645 692 @property
646 693 def AuthUser(self):
647 694 """
648 695 Returns instance of AuthUser for this user
649 696 """
650 697 from rhodecode.lib.auth import AuthUser
651 698 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 699 username=self.username)
653 700
654 701 @hybrid_property
655 702 def user_data(self):
656 703 if not self._user_data:
657 704 return {}
658 705
659 706 try:
660 707 return json.loads(self._user_data)
661 708 except TypeError:
662 709 return {}
663 710
664 711 @user_data.setter
665 712 def user_data(self, val):
666 713 if not isinstance(val, dict):
667 714 raise Exception('user_data must be dict, got %s' % type(val))
668 715 try:
669 716 self._user_data = json.dumps(val)
670 717 except Exception:
671 718 log.error(traceback.format_exc())
672 719
673 720 @classmethod
674 721 def get_by_username(cls, username, case_insensitive=False,
675 722 cache=False, identity_cache=False):
676 723 session = Session()
677 724
678 725 if case_insensitive:
679 726 q = cls.query().filter(
680 727 func.lower(cls.username) == func.lower(username))
681 728 else:
682 729 q = cls.query().filter(cls.username == username)
683 730
684 731 if cache:
685 732 if identity_cache:
686 733 val = cls.identity_cache(session, 'username', username)
687 734 if val:
688 735 return val
689 736 else:
690 737 q = q.options(
691 738 FromCache("sql_cache_short",
692 739 "get_user_by_name_%s" % _hash_key(username)))
693 740
694 741 return q.scalar()
695 742
696 743 @classmethod
697 744 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 745 q = cls.query().filter(cls.api_key == auth_token)
699 746
700 747 if cache:
701 748 q = q.options(FromCache("sql_cache_short",
702 749 "get_auth_token_%s" % auth_token))
703 750 res = q.scalar()
704 751
705 752 if fallback and not res:
706 753 #fallback to additional keys
707 754 _res = UserApiKeys.query()\
708 755 .filter(UserApiKeys.api_key == auth_token)\
709 756 .filter(or_(UserApiKeys.expires == -1,
710 757 UserApiKeys.expires >= time.time()))\
711 758 .first()
712 759 if _res:
713 760 res = _res.user
714 761 return res
715 762
716 763 @classmethod
717 764 def get_by_email(cls, email, case_insensitive=False, cache=False):
718 765
719 766 if case_insensitive:
720 767 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721 768
722 769 else:
723 770 q = cls.query().filter(cls.email == email)
724 771
725 772 if cache:
726 773 q = q.options(FromCache("sql_cache_short",
727 774 "get_email_key_%s" % _hash_key(email)))
728 775
729 776 ret = q.scalar()
730 777 if ret is None:
731 778 q = UserEmailMap.query()
732 779 # try fetching in alternate email map
733 780 if case_insensitive:
734 781 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 782 else:
736 783 q = q.filter(UserEmailMap.email == email)
737 784 q = q.options(joinedload(UserEmailMap.user))
738 785 if cache:
739 786 q = q.options(FromCache("sql_cache_short",
740 787 "get_email_map_key_%s" % email))
741 788 ret = getattr(q.scalar(), 'user', None)
742 789
743 790 return ret
744 791
745 792 @classmethod
746 793 def get_from_cs_author(cls, author):
747 794 """
748 795 Tries to get User objects out of commit author string
749 796
750 797 :param author:
751 798 """
752 799 from rhodecode.lib.helpers import email, author_name
753 800 # Valid email in the attribute passed, see if they're in the system
754 801 _email = email(author)
755 802 if _email:
756 803 user = cls.get_by_email(_email, case_insensitive=True)
757 804 if user:
758 805 return user
759 806 # Maybe we can match by username?
760 807 _author = author_name(author)
761 808 user = cls.get_by_username(_author, case_insensitive=True)
762 809 if user:
763 810 return user
764 811
765 812 def update_userdata(self, **kwargs):
766 813 usr = self
767 814 old = usr.user_data
768 815 old.update(**kwargs)
769 816 usr.user_data = old
770 817 Session().add(usr)
771 818 log.debug('updated userdata with ', kwargs)
772 819
773 820 def update_lastlogin(self):
774 821 """Update user lastlogin"""
775 822 self.last_login = datetime.datetime.now()
776 823 Session().add(self)
777 824 log.debug('updated user %s lastlogin', self.username)
778 825
779 826 def update_lastactivity(self):
780 827 """Update user lastactivity"""
781 828 usr = self
782 829 old = usr.user_data
783 830 old.update({'last_activity': time.time()})
784 831 usr.user_data = old
785 832 Session().add(usr)
786 833 log.debug('updated user %s lastactivity', usr.username)
787 834
788 835 def update_password(self, new_password, change_api_key=False):
789 836 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790 837
791 838 self.password = get_crypt_password(new_password)
792 839 if change_api_key:
793 840 self.api_key = generate_auth_token(self.username)
794 841 Session().add(self)
795 842
796 843 @classmethod
797 844 def get_first_super_admin(cls):
798 845 user = User.query().filter(User.admin == true()).first()
799 846 if user is None:
800 847 raise Exception('FATAL: Missing administrative account!')
801 848 return user
802 849
803 850 @classmethod
804 851 def get_all_super_admins(cls):
805 852 """
806 853 Returns all admin accounts sorted by username
807 854 """
808 855 return User.query().filter(User.admin == true())\
809 856 .order_by(User.username.asc()).all()
810 857
811 858 @classmethod
812 859 def get_default_user(cls, cache=False):
813 860 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 861 if user is None:
815 862 raise Exception('FATAL: Missing default account!')
816 863 return user
817 864
818 865 def _get_default_perms(self, user, suffix=''):
819 866 from rhodecode.model.permission import PermissionModel
820 867 return PermissionModel().get_default_perms(user.user_perms, suffix)
821 868
822 869 def get_default_perms(self, suffix=''):
823 870 return self._get_default_perms(self, suffix)
824 871
825 872 def get_api_data(self, include_secrets=False, details='full'):
826 873 """
827 874 Common function for generating user related data for API
828 875
829 876 :param include_secrets: By default secrets in the API data will be replaced
830 877 by a placeholder value to prevent exposing this data by accident. In case
831 878 this data shall be exposed, set this flag to ``True``.
832 879
833 880 :param details: details can be 'basic|full' basic gives only a subset of
834 881 the available user information that includes user_id, name and emails.
835 882 """
836 883 user = self
837 884 user_data = self.user_data
838 885 data = {
839 886 'user_id': user.user_id,
840 887 'username': user.username,
841 888 'firstname': user.name,
842 889 'lastname': user.lastname,
843 890 'email': user.email,
844 891 'emails': user.emails,
845 892 }
846 893 if details == 'basic':
847 894 return data
848 895
849 896 api_key_length = 40
850 897 api_key_replacement = '*' * api_key_length
851 898
852 899 extras = {
853 900 'api_key': api_key_replacement,
854 901 'api_keys': [api_key_replacement],
855 902 'active': user.active,
856 903 'admin': user.admin,
857 904 'extern_type': user.extern_type,
858 905 'extern_name': user.extern_name,
859 906 'last_login': user.last_login,
860 907 'ip_addresses': user.ip_addresses,
861 908 'language': user_data.get('language')
862 909 }
863 910 data.update(extras)
864 911
865 912 if include_secrets:
866 913 data['api_key'] = user.api_key
867 914 data['api_keys'] = user.auth_tokens
868 915 return data
869 916
870 917 def __json__(self):
871 918 data = {
872 919 'full_name': self.full_name,
873 920 'full_name_or_username': self.full_name_or_username,
874 921 'short_contact': self.short_contact,
875 922 'full_contact': self.full_contact,
876 923 }
877 924 data.update(self.get_api_data())
878 925 return data
879 926
880 927
881 928 class UserApiKeys(Base, BaseModel):
882 929 __tablename__ = 'user_api_keys'
883 930 __table_args__ = (
884 931 Index('uak_api_key_idx', 'api_key'),
885 932 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 933 UniqueConstraint('api_key'),
887 934 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 935 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 936 )
890 937 __mapper_args__ = {}
891 938
892 939 # ApiKey role
893 940 ROLE_ALL = 'token_role_all'
894 941 ROLE_HTTP = 'token_role_http'
895 942 ROLE_VCS = 'token_role_vcs'
896 943 ROLE_API = 'token_role_api'
897 944 ROLE_FEED = 'token_role_feed'
898 945 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899 946
900 947 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 948 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 949 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 950 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 951 expires = Column('expires', Float(53), nullable=False)
905 952 role = Column('role', String(255), nullable=True)
906 953 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907 954
908 955 user = relationship('User', lazy='joined')
909 956
910 957 @classmethod
911 958 def _get_role_name(cls, role):
912 959 return {
913 960 cls.ROLE_ALL: _('all'),
914 961 cls.ROLE_HTTP: _('http/web interface'),
915 962 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 963 cls.ROLE_API: _('api calls'),
917 964 cls.ROLE_FEED: _('feed access'),
918 965 }.get(role, role)
919 966
920 967 @property
921 968 def expired(self):
922 969 if self.expires == -1:
923 970 return False
924 971 return time.time() > self.expires
925 972
926 973 @property
927 974 def role_humanized(self):
928 975 return self._get_role_name(self.role)
929 976
930 977
931 978 class UserEmailMap(Base, BaseModel):
932 979 __tablename__ = 'user_email_map'
933 980 __table_args__ = (
934 981 Index('uem_email_idx', 'email'),
935 982 UniqueConstraint('email'),
936 983 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 984 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 985 )
939 986 __mapper_args__ = {}
940 987
941 988 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 989 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 990 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 991 user = relationship('User', lazy='joined')
945 992
946 993 @validates('_email')
947 994 def validate_email(self, key, email):
948 995 # check if this email is not main one
949 996 main_email = Session().query(User).filter(User.email == email).scalar()
950 997 if main_email is not None:
951 998 raise AttributeError('email %s is present is user table' % email)
952 999 return email
953 1000
954 1001 @hybrid_property
955 1002 def email(self):
956 1003 return self._email
957 1004
958 1005 @email.setter
959 1006 def email(self, val):
960 1007 self._email = val.lower() if val else None
961 1008
962 1009
963 1010 class UserIpMap(Base, BaseModel):
964 1011 __tablename__ = 'user_ip_map'
965 1012 __table_args__ = (
966 1013 UniqueConstraint('user_id', 'ip_addr'),
967 1014 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 1015 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 1016 )
970 1017 __mapper_args__ = {}
971 1018
972 1019 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 1020 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 1021 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 1022 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 1023 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 1024 user = relationship('User', lazy='joined')
978 1025
979 1026 @classmethod
980 1027 def _get_ip_range(cls, ip_addr):
981 1028 net = ipaddress.ip_network(ip_addr, strict=False)
982 1029 return [str(net.network_address), str(net.broadcast_address)]
983 1030
984 1031 def __json__(self):
985 1032 return {
986 1033 'ip_addr': self.ip_addr,
987 1034 'ip_range': self._get_ip_range(self.ip_addr),
988 1035 }
989 1036
990 1037 def __unicode__(self):
991 1038 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 1039 self.user_id, self.ip_addr)
993 1040
994 1041 class UserLog(Base, BaseModel):
995 1042 __tablename__ = 'user_logs'
996 1043 __table_args__ = (
997 1044 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 1045 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 1046 )
1000 1047 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 1048 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 1049 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 1050 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 1051 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 1052 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 1053 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 1054 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008 1055
1009 1056 def __unicode__(self):
1010 1057 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 1058 self.repository_name,
1012 1059 self.action)
1013 1060
1014 1061 @property
1015 1062 def action_as_day(self):
1016 1063 return datetime.date(*self.action_date.timetuple()[:3])
1017 1064
1018 1065 user = relationship('User')
1019 1066 repository = relationship('Repository', cascade='')
1020 1067
1021 1068
1022 1069 class UserGroup(Base, BaseModel):
1023 1070 __tablename__ = 'users_groups'
1024 1071 __table_args__ = (
1025 1072 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 1073 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 1074 )
1028 1075
1029 1076 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 1077 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 1078 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 1079 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 1080 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 1081 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 1082 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 1083 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037 1084
1038 1085 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 1086 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 1087 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 1088 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 1089 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 1090 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044 1091
1045 1092 user = relationship('User')
1046 1093
1047 1094 @hybrid_property
1048 1095 def group_data(self):
1049 1096 if not self._group_data:
1050 1097 return {}
1051 1098
1052 1099 try:
1053 1100 return json.loads(self._group_data)
1054 1101 except TypeError:
1055 1102 return {}
1056 1103
1057 1104 @group_data.setter
1058 1105 def group_data(self, val):
1059 1106 try:
1060 1107 self._group_data = json.dumps(val)
1061 1108 except Exception:
1062 1109 log.error(traceback.format_exc())
1063 1110
1064 1111 def __unicode__(self):
1065 1112 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 1113 self.users_group_id,
1067 1114 self.users_group_name)
1068 1115
1069 1116 @classmethod
1070 1117 def get_by_group_name(cls, group_name, cache=False,
1071 1118 case_insensitive=False):
1072 1119 if case_insensitive:
1073 1120 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 1121 func.lower(group_name))
1075 1122
1076 1123 else:
1077 1124 q = cls.query().filter(cls.users_group_name == group_name)
1078 1125 if cache:
1079 1126 q = q.options(FromCache(
1080 1127 "sql_cache_short",
1081 1128 "get_group_%s" % _hash_key(group_name)))
1082 1129 return q.scalar()
1083 1130
1084 1131 @classmethod
1085 1132 def get(cls, user_group_id, cache=False):
1086 1133 user_group = cls.query()
1087 1134 if cache:
1088 1135 user_group = user_group.options(FromCache("sql_cache_short",
1089 1136 "get_users_group_%s" % user_group_id))
1090 1137 return user_group.get(user_group_id)
1091 1138
1092 1139 def permissions(self, with_admins=True, with_owner=True):
1093 1140 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 1141 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 1142 joinedload(UserUserGroupToPerm.user),
1096 1143 joinedload(UserUserGroupToPerm.permission),)
1097 1144
1098 1145 # get owners and admins and permissions. We do a trick of re-writing
1099 1146 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 1147 # has a global reference and changing one object propagates to all
1101 1148 # others. This means if admin is also an owner admin_row that change
1102 1149 # would propagate to both objects
1103 1150 perm_rows = []
1104 1151 for _usr in q.all():
1105 1152 usr = AttributeDict(_usr.user.get_dict())
1106 1153 usr.permission = _usr.permission.permission_name
1107 1154 perm_rows.append(usr)
1108 1155
1109 1156 # filter the perm rows by 'default' first and then sort them by
1110 1157 # admin,write,read,none permissions sorted again alphabetically in
1111 1158 # each group
1112 1159 perm_rows = sorted(perm_rows, key=display_sort)
1113 1160
1114 1161 _admin_perm = 'usergroup.admin'
1115 1162 owner_row = []
1116 1163 if with_owner:
1117 1164 usr = AttributeDict(self.user.get_dict())
1118 1165 usr.owner_row = True
1119 1166 usr.permission = _admin_perm
1120 1167 owner_row.append(usr)
1121 1168
1122 1169 super_admin_rows = []
1123 1170 if with_admins:
1124 1171 for usr in User.get_all_super_admins():
1125 1172 # if this admin is also owner, don't double the record
1126 1173 if usr.user_id == owner_row[0].user_id:
1127 1174 owner_row[0].admin_row = True
1128 1175 else:
1129 1176 usr = AttributeDict(usr.get_dict())
1130 1177 usr.admin_row = True
1131 1178 usr.permission = _admin_perm
1132 1179 super_admin_rows.append(usr)
1133 1180
1134 1181 return super_admin_rows + owner_row + perm_rows
1135 1182
1136 1183 def permission_user_groups(self):
1137 1184 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 1185 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 1186 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 1187 joinedload(UserGroupUserGroupToPerm.permission),)
1141 1188
1142 1189 perm_rows = []
1143 1190 for _user_group in q.all():
1144 1191 usr = AttributeDict(_user_group.user_group.get_dict())
1145 1192 usr.permission = _user_group.permission.permission_name
1146 1193 perm_rows.append(usr)
1147 1194
1148 1195 return perm_rows
1149 1196
1150 1197 def _get_default_perms(self, user_group, suffix=''):
1151 1198 from rhodecode.model.permission import PermissionModel
1152 1199 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153 1200
1154 1201 def get_default_perms(self, suffix=''):
1155 1202 return self._get_default_perms(self, suffix)
1156 1203
1157 1204 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 1205 """
1159 1206 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 1207 basically forwarded.
1161 1208
1162 1209 """
1163 1210 user_group = self
1164 1211
1165 1212 data = {
1166 1213 'users_group_id': user_group.users_group_id,
1167 1214 'group_name': user_group.users_group_name,
1168 1215 'group_description': user_group.user_group_description,
1169 1216 'active': user_group.users_group_active,
1170 1217 'owner': user_group.user.username,
1171 1218 }
1172 1219 if with_group_members:
1173 1220 users = []
1174 1221 for user in user_group.members:
1175 1222 user = user.user
1176 1223 users.append(user.get_api_data(include_secrets=include_secrets))
1177 1224 data['users'] = users
1178 1225
1179 1226 return data
1180 1227
1181 1228
1182 1229 class UserGroupMember(Base, BaseModel):
1183 1230 __tablename__ = 'users_groups_members'
1184 1231 __table_args__ = (
1185 1232 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 1233 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 1234 )
1188 1235
1189 1236 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 1237 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 1238 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192 1239
1193 1240 user = relationship('User', lazy='joined')
1194 1241 users_group = relationship('UserGroup')
1195 1242
1196 1243 def __init__(self, gr_id='', u_id=''):
1197 1244 self.users_group_id = gr_id
1198 1245 self.user_id = u_id
1199 1246
1200 1247
1201 1248 class RepositoryField(Base, BaseModel):
1202 1249 __tablename__ = 'repositories_fields'
1203 1250 __table_args__ = (
1204 1251 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 1252 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 1253 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 1254 )
1208 1255 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209 1256
1210 1257 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 1258 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 1259 field_key = Column("field_key", String(250))
1213 1260 field_label = Column("field_label", String(1024), nullable=False)
1214 1261 field_value = Column("field_value", String(10000), nullable=False)
1215 1262 field_desc = Column("field_desc", String(1024), nullable=False)
1216 1263 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 1264 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218 1265
1219 1266 repository = relationship('Repository')
1220 1267
1221 1268 @property
1222 1269 def field_key_prefixed(self):
1223 1270 return 'ex_%s' % self.field_key
1224 1271
1225 1272 @classmethod
1226 1273 def un_prefix_key(cls, key):
1227 1274 if key.startswith(cls.PREFIX):
1228 1275 return key[len(cls.PREFIX):]
1229 1276 return key
1230 1277
1231 1278 @classmethod
1232 1279 def get_by_key_name(cls, key, repo):
1233 1280 row = cls.query()\
1234 1281 .filter(cls.repository == repo)\
1235 1282 .filter(cls.field_key == key).scalar()
1236 1283 return row
1237 1284
1238 1285
1239 1286 class Repository(Base, BaseModel):
1240 1287 __tablename__ = 'repositories'
1241 1288 __table_args__ = (
1242 1289 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 1290 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 1291 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 1292 )
1246 1293 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 1294 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248 1295
1249 1296 STATE_CREATED = 'repo_state_created'
1250 1297 STATE_PENDING = 'repo_state_pending'
1251 1298 STATE_ERROR = 'repo_state_error'
1252 1299
1253 1300 LOCK_AUTOMATIC = 'lock_auto'
1254 1301 LOCK_API = 'lock_api'
1255 1302 LOCK_WEB = 'lock_web'
1256 1303 LOCK_PULL = 'lock_pull'
1257 1304
1258 1305 NAME_SEP = URL_SEP
1259 1306
1260 1307 repo_id = Column(
1261 1308 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 1309 primary_key=True)
1263 1310 _repo_name = Column(
1264 1311 "repo_name", Text(), nullable=False, default=None)
1265 1312 _repo_name_hash = Column(
1266 1313 "repo_name_hash", String(255), nullable=False, unique=True)
1267 1314 repo_state = Column("repo_state", String(255), nullable=True)
1268 1315
1269 1316 clone_uri = Column(
1270 1317 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 1318 default=None)
1272 1319 repo_type = Column(
1273 1320 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 1321 user_id = Column(
1275 1322 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 1323 unique=False, default=None)
1277 1324 private = Column(
1278 1325 "private", Boolean(), nullable=True, unique=None, default=None)
1279 1326 enable_statistics = Column(
1280 1327 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 1328 enable_downloads = Column(
1282 1329 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 1330 description = Column(
1284 1331 "description", String(10000), nullable=True, unique=None, default=None)
1285 1332 created_on = Column(
1286 1333 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1334 default=datetime.datetime.now)
1288 1335 updated_on = Column(
1289 1336 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 1337 default=datetime.datetime.now)
1291 1338 _landing_revision = Column(
1292 1339 "landing_revision", String(255), nullable=False, unique=False,
1293 1340 default=None)
1294 1341 enable_locking = Column(
1295 1342 "enable_locking", Boolean(), nullable=False, unique=None,
1296 1343 default=False)
1297 1344 _locked = Column(
1298 1345 "locked", String(255), nullable=True, unique=False, default=None)
1299 1346 _changeset_cache = Column(
1300 1347 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301 1348
1302 1349 fork_id = Column(
1303 1350 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 1351 nullable=True, unique=False, default=None)
1305 1352 group_id = Column(
1306 1353 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 1354 unique=False, default=None)
1308 1355
1309 1356 user = relationship('User', lazy='joined')
1310 1357 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 1358 group = relationship('RepoGroup', lazy='joined')
1312 1359 repo_to_perm = relationship(
1313 1360 'UserRepoToPerm', cascade='all',
1314 1361 order_by='UserRepoToPerm.repo_to_perm_id')
1315 1362 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 1363 stats = relationship('Statistics', cascade='all', uselist=False)
1317 1364
1318 1365 followers = relationship(
1319 1366 'UserFollowing',
1320 1367 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 1368 cascade='all')
1322 1369 extra_fields = relationship(
1323 1370 'RepositoryField', cascade="all, delete, delete-orphan")
1324 1371 logs = relationship('UserLog')
1325 1372 comments = relationship(
1326 1373 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 1374 pull_requests_source = relationship(
1328 1375 'PullRequest',
1329 1376 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 1377 cascade="all, delete, delete-orphan")
1331 1378 pull_requests_target = relationship(
1332 1379 'PullRequest',
1333 1380 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 1381 cascade="all, delete, delete-orphan")
1335 1382 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 1383 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 1384 integrations = relationship('Integration',
1338 1385 cascade="all, delete, delete-orphan")
1339 1386
1340 1387 def __unicode__(self):
1341 1388 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 1389 safe_unicode(self.repo_name))
1343 1390
1344 1391 @hybrid_property
1345 1392 def landing_rev(self):
1346 1393 # always should return [rev_type, rev]
1347 1394 if self._landing_revision:
1348 1395 _rev_info = self._landing_revision.split(':')
1349 1396 if len(_rev_info) < 2:
1350 1397 _rev_info.insert(0, 'rev')
1351 1398 return [_rev_info[0], _rev_info[1]]
1352 1399 return [None, None]
1353 1400
1354 1401 @landing_rev.setter
1355 1402 def landing_rev(self, val):
1356 1403 if ':' not in val:
1357 1404 raise ValueError('value must be delimited with `:` and consist '
1358 1405 'of <rev_type>:<rev>, got %s instead' % val)
1359 1406 self._landing_revision = val
1360 1407
1361 1408 @hybrid_property
1362 1409 def locked(self):
1363 1410 if self._locked:
1364 1411 user_id, timelocked, reason = self._locked.split(':')
1365 1412 lock_values = int(user_id), timelocked, reason
1366 1413 else:
1367 1414 lock_values = [None, None, None]
1368 1415 return lock_values
1369 1416
1370 1417 @locked.setter
1371 1418 def locked(self, val):
1372 1419 if val and isinstance(val, (list, tuple)):
1373 1420 self._locked = ':'.join(map(str, val))
1374 1421 else:
1375 1422 self._locked = None
1376 1423
1377 1424 @hybrid_property
1378 1425 def changeset_cache(self):
1379 1426 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 1427 dummy = EmptyCommit().__json__()
1381 1428 if not self._changeset_cache:
1382 1429 return dummy
1383 1430 try:
1384 1431 return json.loads(self._changeset_cache)
1385 1432 except TypeError:
1386 1433 return dummy
1387 1434 except Exception:
1388 1435 log.error(traceback.format_exc())
1389 1436 return dummy
1390 1437
1391 1438 @changeset_cache.setter
1392 1439 def changeset_cache(self, val):
1393 1440 try:
1394 1441 self._changeset_cache = json.dumps(val)
1395 1442 except Exception:
1396 1443 log.error(traceback.format_exc())
1397 1444
1398 1445 @hybrid_property
1399 1446 def repo_name(self):
1400 1447 return self._repo_name
1401 1448
1402 1449 @repo_name.setter
1403 1450 def repo_name(self, value):
1404 1451 self._repo_name = value
1405 1452 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406 1453
1407 1454 @classmethod
1408 1455 def normalize_repo_name(cls, repo_name):
1409 1456 """
1410 1457 Normalizes os specific repo_name to the format internally stored inside
1411 1458 database using URL_SEP
1412 1459
1413 1460 :param cls:
1414 1461 :param repo_name:
1415 1462 """
1416 1463 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417 1464
1418 1465 @classmethod
1419 1466 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 1467 session = Session()
1421 1468 q = session.query(cls).filter(cls.repo_name == repo_name)
1422 1469
1423 1470 if cache:
1424 1471 if identity_cache:
1425 1472 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 1473 if val:
1427 1474 return val
1428 1475 else:
1429 1476 q = q.options(
1430 1477 FromCache("sql_cache_short",
1431 1478 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432 1479
1433 1480 return q.scalar()
1434 1481
1435 1482 @classmethod
1436 1483 def get_by_full_path(cls, repo_full_path):
1437 1484 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 1485 repo_name = cls.normalize_repo_name(repo_name)
1439 1486 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440 1487
1441 1488 @classmethod
1442 1489 def get_repo_forks(cls, repo_id):
1443 1490 return cls.query().filter(Repository.fork_id == repo_id)
1444 1491
1445 1492 @classmethod
1446 1493 def base_path(cls):
1447 1494 """
1448 1495 Returns base path when all repos are stored
1449 1496
1450 1497 :param cls:
1451 1498 """
1452 1499 q = Session().query(RhodeCodeUi)\
1453 1500 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 1501 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 1502 return q.one().ui_value
1456 1503
1457 1504 @classmethod
1458 1505 def is_valid(cls, repo_name):
1459 1506 """
1460 1507 returns True if given repo name is a valid filesystem repository
1461 1508
1462 1509 :param cls:
1463 1510 :param repo_name:
1464 1511 """
1465 1512 from rhodecode.lib.utils import is_valid_repo
1466 1513
1467 1514 return is_valid_repo(repo_name, cls.base_path())
1468 1515
1469 1516 @classmethod
1470 1517 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 1518 case_insensitive=True):
1472 1519 q = Repository.query()
1473 1520
1474 1521 if not isinstance(user_id, Optional):
1475 1522 q = q.filter(Repository.user_id == user_id)
1476 1523
1477 1524 if not isinstance(group_id, Optional):
1478 1525 q = q.filter(Repository.group_id == group_id)
1479 1526
1480 1527 if case_insensitive:
1481 1528 q = q.order_by(func.lower(Repository.repo_name))
1482 1529 else:
1483 1530 q = q.order_by(Repository.repo_name)
1484 1531 return q.all()
1485 1532
1486 1533 @property
1487 1534 def forks(self):
1488 1535 """
1489 1536 Return forks of this repo
1490 1537 """
1491 1538 return Repository.get_repo_forks(self.repo_id)
1492 1539
1493 1540 @property
1494 1541 def parent(self):
1495 1542 """
1496 1543 Returns fork parent
1497 1544 """
1498 1545 return self.fork
1499 1546
1500 1547 @property
1501 1548 def just_name(self):
1502 1549 return self.repo_name.split(self.NAME_SEP)[-1]
1503 1550
1504 1551 @property
1505 1552 def groups_with_parents(self):
1506 1553 groups = []
1507 1554 if self.group is None:
1508 1555 return groups
1509 1556
1510 1557 cur_gr = self.group
1511 1558 groups.insert(0, cur_gr)
1512 1559 while 1:
1513 1560 gr = getattr(cur_gr, 'parent_group', None)
1514 1561 cur_gr = cur_gr.parent_group
1515 1562 if gr is None:
1516 1563 break
1517 1564 groups.insert(0, gr)
1518 1565
1519 1566 return groups
1520 1567
1521 1568 @property
1522 1569 def groups_and_repo(self):
1523 1570 return self.groups_with_parents, self
1524 1571
1525 1572 @LazyProperty
1526 1573 def repo_path(self):
1527 1574 """
1528 1575 Returns base full path for that repository means where it actually
1529 1576 exists on a filesystem
1530 1577 """
1531 1578 q = Session().query(RhodeCodeUi).filter(
1532 1579 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 1580 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 1581 return q.one().ui_value
1535 1582
1536 1583 @property
1537 1584 def repo_full_path(self):
1538 1585 p = [self.repo_path]
1539 1586 # we need to split the name by / since this is how we store the
1540 1587 # names in the database, but that eventually needs to be converted
1541 1588 # into a valid system path
1542 1589 p += self.repo_name.split(self.NAME_SEP)
1543 1590 return os.path.join(*map(safe_unicode, p))
1544 1591
1545 1592 @property
1546 1593 def cache_keys(self):
1547 1594 """
1548 1595 Returns associated cache keys for that repo
1549 1596 """
1550 1597 return CacheKey.query()\
1551 1598 .filter(CacheKey.cache_args == self.repo_name)\
1552 1599 .order_by(CacheKey.cache_key)\
1553 1600 .all()
1554 1601
1555 1602 def get_new_name(self, repo_name):
1556 1603 """
1557 1604 returns new full repository name based on assigned group and new new
1558 1605
1559 1606 :param group_name:
1560 1607 """
1561 1608 path_prefix = self.group.full_path_splitted if self.group else []
1562 1609 return self.NAME_SEP.join(path_prefix + [repo_name])
1563 1610
1564 1611 @property
1565 1612 def _config(self):
1566 1613 """
1567 1614 Returns db based config object.
1568 1615 """
1569 1616 from rhodecode.lib.utils import make_db_config
1570 1617 return make_db_config(clear_session=False, repo=self)
1571 1618
1572 1619 def permissions(self, with_admins=True, with_owner=True):
1573 1620 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 1621 q = q.options(joinedload(UserRepoToPerm.repository),
1575 1622 joinedload(UserRepoToPerm.user),
1576 1623 joinedload(UserRepoToPerm.permission),)
1577 1624
1578 1625 # get owners and admins and permissions. We do a trick of re-writing
1579 1626 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 1627 # has a global reference and changing one object propagates to all
1581 1628 # others. This means if admin is also an owner admin_row that change
1582 1629 # would propagate to both objects
1583 1630 perm_rows = []
1584 1631 for _usr in q.all():
1585 1632 usr = AttributeDict(_usr.user.get_dict())
1586 1633 usr.permission = _usr.permission.permission_name
1587 1634 perm_rows.append(usr)
1588 1635
1589 1636 # filter the perm rows by 'default' first and then sort them by
1590 1637 # admin,write,read,none permissions sorted again alphabetically in
1591 1638 # each group
1592 1639 perm_rows = sorted(perm_rows, key=display_sort)
1593 1640
1594 1641 _admin_perm = 'repository.admin'
1595 1642 owner_row = []
1596 1643 if with_owner:
1597 1644 usr = AttributeDict(self.user.get_dict())
1598 1645 usr.owner_row = True
1599 1646 usr.permission = _admin_perm
1600 1647 owner_row.append(usr)
1601 1648
1602 1649 super_admin_rows = []
1603 1650 if with_admins:
1604 1651 for usr in User.get_all_super_admins():
1605 1652 # if this admin is also owner, don't double the record
1606 1653 if usr.user_id == owner_row[0].user_id:
1607 1654 owner_row[0].admin_row = True
1608 1655 else:
1609 1656 usr = AttributeDict(usr.get_dict())
1610 1657 usr.admin_row = True
1611 1658 usr.permission = _admin_perm
1612 1659 super_admin_rows.append(usr)
1613 1660
1614 1661 return super_admin_rows + owner_row + perm_rows
1615 1662
1616 1663 def permission_user_groups(self):
1617 1664 q = UserGroupRepoToPerm.query().filter(
1618 1665 UserGroupRepoToPerm.repository == self)
1619 1666 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 1667 joinedload(UserGroupRepoToPerm.users_group),
1621 1668 joinedload(UserGroupRepoToPerm.permission),)
1622 1669
1623 1670 perm_rows = []
1624 1671 for _user_group in q.all():
1625 1672 usr = AttributeDict(_user_group.users_group.get_dict())
1626 1673 usr.permission = _user_group.permission.permission_name
1627 1674 perm_rows.append(usr)
1628 1675
1629 1676 return perm_rows
1630 1677
1631 1678 def get_api_data(self, include_secrets=False):
1632 1679 """
1633 1680 Common function for generating repo api data
1634 1681
1635 1682 :param include_secrets: See :meth:`User.get_api_data`.
1636 1683
1637 1684 """
1638 1685 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 1686 # move this methods on models level.
1640 1687 from rhodecode.model.settings import SettingsModel
1641 1688
1642 1689 repo = self
1643 1690 _user_id, _time, _reason = self.locked
1644 1691
1645 1692 data = {
1646 1693 'repo_id': repo.repo_id,
1647 1694 'repo_name': repo.repo_name,
1648 1695 'repo_type': repo.repo_type,
1649 1696 'clone_uri': repo.clone_uri or '',
1650 1697 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 1698 'private': repo.private,
1652 1699 'created_on': repo.created_on,
1653 1700 'description': repo.description,
1654 1701 'landing_rev': repo.landing_rev,
1655 1702 'owner': repo.user.username,
1656 1703 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 1704 'enable_statistics': repo.enable_statistics,
1658 1705 'enable_locking': repo.enable_locking,
1659 1706 'enable_downloads': repo.enable_downloads,
1660 1707 'last_changeset': repo.changeset_cache,
1661 1708 'locked_by': User.get(_user_id).get_api_data(
1662 1709 include_secrets=include_secrets) if _user_id else None,
1663 1710 'locked_date': time_to_datetime(_time) if _time else None,
1664 1711 'lock_reason': _reason if _reason else None,
1665 1712 }
1666 1713
1667 1714 # TODO: mikhail: should be per-repo settings here
1668 1715 rc_config = SettingsModel().get_all_settings()
1669 1716 repository_fields = str2bool(
1670 1717 rc_config.get('rhodecode_repository_fields'))
1671 1718 if repository_fields:
1672 1719 for f in self.extra_fields:
1673 1720 data[f.field_key_prefixed] = f.field_value
1674 1721
1675 1722 return data
1676 1723
1677 1724 @classmethod
1678 1725 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 1726 if not lock_time:
1680 1727 lock_time = time.time()
1681 1728 if not lock_reason:
1682 1729 lock_reason = cls.LOCK_AUTOMATIC
1683 1730 repo.locked = [user_id, lock_time, lock_reason]
1684 1731 Session().add(repo)
1685 1732 Session().commit()
1686 1733
1687 1734 @classmethod
1688 1735 def unlock(cls, repo):
1689 1736 repo.locked = None
1690 1737 Session().add(repo)
1691 1738 Session().commit()
1692 1739
1693 1740 @classmethod
1694 1741 def getlock(cls, repo):
1695 1742 return repo.locked
1696 1743
1697 1744 def is_user_lock(self, user_id):
1698 1745 if self.lock[0]:
1699 1746 lock_user_id = safe_int(self.lock[0])
1700 1747 user_id = safe_int(user_id)
1701 1748 # both are ints, and they are equal
1702 1749 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703 1750
1704 1751 return False
1705 1752
1706 1753 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 1754 """
1708 1755 Checks locking on this repository, if locking is enabled and lock is
1709 1756 present returns a tuple of make_lock, locked, locked_by.
1710 1757 make_lock can have 3 states None (do nothing) True, make lock
1711 1758 False release lock, This value is later propagated to hooks, which
1712 1759 do the locking. Think about this as signals passed to hooks what to do.
1713 1760
1714 1761 """
1715 1762 # TODO: johbo: This is part of the business logic and should be moved
1716 1763 # into the RepositoryModel.
1717 1764
1718 1765 if action not in ('push', 'pull'):
1719 1766 raise ValueError("Invalid action value: %s" % repr(action))
1720 1767
1721 1768 # defines if locked error should be thrown to user
1722 1769 currently_locked = False
1723 1770 # defines if new lock should be made, tri-state
1724 1771 make_lock = None
1725 1772 repo = self
1726 1773 user = User.get(user_id)
1727 1774
1728 1775 lock_info = repo.locked
1729 1776
1730 1777 if repo and (repo.enable_locking or not only_when_enabled):
1731 1778 if action == 'push':
1732 1779 # check if it's already locked !, if it is compare users
1733 1780 locked_by_user_id = lock_info[0]
1734 1781 if user.user_id == locked_by_user_id:
1735 1782 log.debug(
1736 1783 'Got `push` action from user %s, now unlocking', user)
1737 1784 # unlock if we have push from user who locked
1738 1785 make_lock = False
1739 1786 else:
1740 1787 # we're not the same user who locked, ban with
1741 1788 # code defined in settings (default is 423 HTTP Locked) !
1742 1789 log.debug('Repo %s is currently locked by %s', repo, user)
1743 1790 currently_locked = True
1744 1791 elif action == 'pull':
1745 1792 # [0] user [1] date
1746 1793 if lock_info[0] and lock_info[1]:
1747 1794 log.debug('Repo %s is currently locked by %s', repo, user)
1748 1795 currently_locked = True
1749 1796 else:
1750 1797 log.debug('Setting lock on repo %s by %s', repo, user)
1751 1798 make_lock = True
1752 1799
1753 1800 else:
1754 1801 log.debug('Repository %s do not have locking enabled', repo)
1755 1802
1756 1803 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 1804 make_lock, currently_locked, lock_info)
1758 1805
1759 1806 from rhodecode.lib.auth import HasRepoPermissionAny
1760 1807 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 1808 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 1809 # if we don't have at least write permission we cannot make a lock
1763 1810 log.debug('lock state reset back to FALSE due to lack '
1764 1811 'of at least read permission')
1765 1812 make_lock = False
1766 1813
1767 1814 return make_lock, currently_locked, lock_info
1768 1815
1769 1816 @property
1770 1817 def last_db_change(self):
1771 1818 return self.updated_on
1772 1819
1773 1820 @property
1774 1821 def clone_uri_hidden(self):
1775 1822 clone_uri = self.clone_uri
1776 1823 if clone_uri:
1777 1824 import urlobject
1778 1825 url_obj = urlobject.URLObject(clone_uri)
1779 1826 if url_obj.password:
1780 1827 clone_uri = url_obj.with_password('*****')
1781 1828 return clone_uri
1782 1829
1783 1830 def clone_url(self, **override):
1784 1831 qualified_home_url = url('home', qualified=True)
1785 1832
1786 1833 uri_tmpl = None
1787 1834 if 'with_id' in override:
1788 1835 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 1836 del override['with_id']
1790 1837
1791 1838 if 'uri_tmpl' in override:
1792 1839 uri_tmpl = override['uri_tmpl']
1793 1840 del override['uri_tmpl']
1794 1841
1795 1842 # we didn't override our tmpl from **overrides
1796 1843 if not uri_tmpl:
1797 1844 uri_tmpl = self.DEFAULT_CLONE_URI
1798 1845 try:
1799 1846 from pylons import tmpl_context as c
1800 1847 uri_tmpl = c.clone_uri_tmpl
1801 1848 except Exception:
1802 1849 # in any case if we call this outside of request context,
1803 1850 # ie, not having tmpl_context set up
1804 1851 pass
1805 1852
1806 1853 return get_clone_url(uri_tmpl=uri_tmpl,
1807 1854 qualifed_home_url=qualified_home_url,
1808 1855 repo_name=self.repo_name,
1809 1856 repo_id=self.repo_id, **override)
1810 1857
1811 1858 def set_state(self, state):
1812 1859 self.repo_state = state
1813 1860 Session().add(self)
1814 1861 #==========================================================================
1815 1862 # SCM PROPERTIES
1816 1863 #==========================================================================
1817 1864
1818 1865 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 1866 return get_commit_safe(
1820 1867 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821 1868
1822 1869 def get_changeset(self, rev=None, pre_load=None):
1823 1870 warnings.warn("Use get_commit", DeprecationWarning)
1824 1871 commit_id = None
1825 1872 commit_idx = None
1826 1873 if isinstance(rev, basestring):
1827 1874 commit_id = rev
1828 1875 else:
1829 1876 commit_idx = rev
1830 1877 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 1878 pre_load=pre_load)
1832 1879
1833 1880 def get_landing_commit(self):
1834 1881 """
1835 1882 Returns landing commit, or if that doesn't exist returns the tip
1836 1883 """
1837 1884 _rev_type, _rev = self.landing_rev
1838 1885 commit = self.get_commit(_rev)
1839 1886 if isinstance(commit, EmptyCommit):
1840 1887 return self.get_commit()
1841 1888 return commit
1842 1889
1843 1890 def update_commit_cache(self, cs_cache=None, config=None):
1844 1891 """
1845 1892 Update cache of last changeset for repository, keys should be::
1846 1893
1847 1894 short_id
1848 1895 raw_id
1849 1896 revision
1850 1897 parents
1851 1898 message
1852 1899 date
1853 1900 author
1854 1901
1855 1902 :param cs_cache:
1856 1903 """
1857 1904 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 1905 if cs_cache is None:
1859 1906 # use no-cache version here
1860 1907 scm_repo = self.scm_instance(cache=False, config=config)
1861 1908 if scm_repo:
1862 1909 cs_cache = scm_repo.get_commit(
1863 1910 pre_load=["author", "date", "message", "parents"])
1864 1911 else:
1865 1912 cs_cache = EmptyCommit()
1866 1913
1867 1914 if isinstance(cs_cache, BaseChangeset):
1868 1915 cs_cache = cs_cache.__json__()
1869 1916
1870 1917 def is_outdated(new_cs_cache):
1871 1918 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 1919 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 1920 return True
1874 1921 return False
1875 1922
1876 1923 # check if we have maybe already latest cached revision
1877 1924 if is_outdated(cs_cache) or not self.changeset_cache:
1878 1925 _default = datetime.datetime.fromtimestamp(0)
1879 1926 last_change = cs_cache.get('date') or _default
1880 1927 log.debug('updated repo %s with new cs cache %s',
1881 1928 self.repo_name, cs_cache)
1882 1929 self.updated_on = last_change
1883 1930 self.changeset_cache = cs_cache
1884 1931 Session().add(self)
1885 1932 Session().commit()
1886 1933 else:
1887 1934 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 1935 'commit already with latest changes', self.repo_name)
1889 1936
1890 1937 @property
1891 1938 def tip(self):
1892 1939 return self.get_commit('tip')
1893 1940
1894 1941 @property
1895 1942 def author(self):
1896 1943 return self.tip.author
1897 1944
1898 1945 @property
1899 1946 def last_change(self):
1900 1947 return self.scm_instance().last_change
1901 1948
1902 1949 def get_comments(self, revisions=None):
1903 1950 """
1904 1951 Returns comments for this repository grouped by revisions
1905 1952
1906 1953 :param revisions: filter query by revisions only
1907 1954 """
1908 1955 cmts = ChangesetComment.query()\
1909 1956 .filter(ChangesetComment.repo == self)
1910 1957 if revisions:
1911 1958 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 1959 grouped = collections.defaultdict(list)
1913 1960 for cmt in cmts.all():
1914 1961 grouped[cmt.revision].append(cmt)
1915 1962 return grouped
1916 1963
1917 1964 def statuses(self, revisions=None):
1918 1965 """
1919 1966 Returns statuses for this repository
1920 1967
1921 1968 :param revisions: list of revisions to get statuses for
1922 1969 """
1923 1970 statuses = ChangesetStatus.query()\
1924 1971 .filter(ChangesetStatus.repo == self)\
1925 1972 .filter(ChangesetStatus.version == 0)
1926 1973
1927 1974 if revisions:
1928 1975 # Try doing the filtering in chunks to avoid hitting limits
1929 1976 size = 500
1930 1977 status_results = []
1931 1978 for chunk in xrange(0, len(revisions), size):
1932 1979 status_results += statuses.filter(
1933 1980 ChangesetStatus.revision.in_(
1934 1981 revisions[chunk: chunk+size])
1935 1982 ).all()
1936 1983 else:
1937 1984 status_results = statuses.all()
1938 1985
1939 1986 grouped = {}
1940 1987
1941 1988 # maybe we have open new pullrequest without a status?
1942 1989 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 1990 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 1991 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 1992 for rev in pr.revisions:
1946 1993 pr_id = pr.pull_request_id
1947 1994 pr_repo = pr.target_repo.repo_name
1948 1995 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949 1996
1950 1997 for stat in status_results:
1951 1998 pr_id = pr_repo = None
1952 1999 if stat.pull_request:
1953 2000 pr_id = stat.pull_request.pull_request_id
1954 2001 pr_repo = stat.pull_request.target_repo.repo_name
1955 2002 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 2003 pr_id, pr_repo]
1957 2004 return grouped
1958 2005
1959 2006 # ==========================================================================
1960 2007 # SCM CACHE INSTANCE
1961 2008 # ==========================================================================
1962 2009
1963 2010 def scm_instance(self, **kwargs):
1964 2011 import rhodecode
1965 2012
1966 2013 # Passing a config will not hit the cache currently only used
1967 2014 # for repo2dbmapper
1968 2015 config = kwargs.pop('config', None)
1969 2016 cache = kwargs.pop('cache', None)
1970 2017 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 2018 # if cache is NOT defined use default global, else we have a full
1972 2019 # control over cache behaviour
1973 2020 if cache is None and full_cache and not config:
1974 2021 return self._get_instance_cached()
1975 2022 return self._get_instance(cache=bool(cache), config=config)
1976 2023
1977 2024 def _get_instance_cached(self):
1978 2025 @cache_region('long_term')
1979 2026 def _get_repo(cache_key):
1980 2027 return self._get_instance()
1981 2028
1982 2029 invalidator_context = CacheKey.repo_context_cache(
1983 2030 _get_repo, self.repo_name, None, thread_scoped=True)
1984 2031
1985 2032 with invalidator_context as context:
1986 2033 context.invalidate()
1987 2034 repo = context.compute()
1988 2035
1989 2036 return repo
1990 2037
1991 2038 def _get_instance(self, cache=True, config=None):
1992 2039 config = config or self._config
1993 2040 custom_wire = {
1994 2041 'cache': cache # controls the vcs.remote cache
1995 2042 }
1996 2043 repo = get_vcs_instance(
1997 2044 repo_path=safe_str(self.repo_full_path),
1998 2045 config=config,
1999 2046 with_wire=custom_wire,
2000 2047 create=False,
2001 2048 _vcs_alias=self.repo_type)
2002 2049
2003 2050 return repo
2004 2051
2005 2052 def __json__(self):
2006 2053 return {'landing_rev': self.landing_rev}
2007 2054
2008 2055 def get_dict(self):
2009 2056
2010 2057 # Since we transformed `repo_name` to a hybrid property, we need to
2011 2058 # keep compatibility with the code which uses `repo_name` field.
2012 2059
2013 2060 result = super(Repository, self).get_dict()
2014 2061 result['repo_name'] = result.pop('_repo_name', None)
2015 2062 return result
2016 2063
2017 2064
2018 2065 class RepoGroup(Base, BaseModel):
2019 2066 __tablename__ = 'groups'
2020 2067 __table_args__ = (
2021 2068 UniqueConstraint('group_name', 'group_parent_id'),
2022 2069 CheckConstraint('group_id != group_parent_id'),
2023 2070 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 2071 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 2072 )
2026 2073 __mapper_args__ = {'order_by': 'group_name'}
2027 2074
2028 2075 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029 2076
2030 2077 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 2078 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 2079 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 2080 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 2081 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 2082 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 2083 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 2084 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038 2085
2039 2086 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 2087 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 2088 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 2089 user = relationship('User')
2043 2090 integrations = relationship('Integration',
2044 2091 cascade="all, delete, delete-orphan")
2045 2092
2046 2093 def __init__(self, group_name='', parent_group=None):
2047 2094 self.group_name = group_name
2048 2095 self.parent_group = parent_group
2049 2096
2050 2097 def __unicode__(self):
2051 2098 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 2099 self.group_name)
2053 2100
2054 2101 @classmethod
2055 2102 def _generate_choice(cls, repo_group):
2056 2103 from webhelpers.html import literal as _literal
2057 2104 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 2105 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059 2106
2060 2107 @classmethod
2061 2108 def groups_choices(cls, groups=None, show_empty_group=True):
2062 2109 if not groups:
2063 2110 groups = cls.query().all()
2064 2111
2065 2112 repo_groups = []
2066 2113 if show_empty_group:
2067 2114 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068 2115
2069 2116 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070 2117
2071 2118 repo_groups = sorted(
2072 2119 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 2120 return repo_groups
2074 2121
2075 2122 @classmethod
2076 2123 def url_sep(cls):
2077 2124 return URL_SEP
2078 2125
2079 2126 @classmethod
2080 2127 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 2128 if case_insensitive:
2082 2129 gr = cls.query().filter(func.lower(cls.group_name)
2083 2130 == func.lower(group_name))
2084 2131 else:
2085 2132 gr = cls.query().filter(cls.group_name == group_name)
2086 2133 if cache:
2087 2134 gr = gr.options(FromCache(
2088 2135 "sql_cache_short",
2089 2136 "get_group_%s" % _hash_key(group_name)))
2090 2137 return gr.scalar()
2091 2138
2092 2139 @classmethod
2093 2140 def get_user_personal_repo_group(cls, user_id):
2094 2141 user = User.get(user_id)
2095 2142 return cls.query()\
2096 2143 .filter(cls.personal == true())\
2097 2144 .filter(cls.user == user).scalar()
2098 2145
2099 2146 @classmethod
2100 2147 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 2148 case_insensitive=True):
2102 2149 q = RepoGroup.query()
2103 2150
2104 2151 if not isinstance(user_id, Optional):
2105 2152 q = q.filter(RepoGroup.user_id == user_id)
2106 2153
2107 2154 if not isinstance(group_id, Optional):
2108 2155 q = q.filter(RepoGroup.group_parent_id == group_id)
2109 2156
2110 2157 if case_insensitive:
2111 2158 q = q.order_by(func.lower(RepoGroup.group_name))
2112 2159 else:
2113 2160 q = q.order_by(RepoGroup.group_name)
2114 2161 return q.all()
2115 2162
2116 2163 @property
2117 2164 def parents(self):
2118 2165 parents_recursion_limit = 10
2119 2166 groups = []
2120 2167 if self.parent_group is None:
2121 2168 return groups
2122 2169 cur_gr = self.parent_group
2123 2170 groups.insert(0, cur_gr)
2124 2171 cnt = 0
2125 2172 while 1:
2126 2173 cnt += 1
2127 2174 gr = getattr(cur_gr, 'parent_group', None)
2128 2175 cur_gr = cur_gr.parent_group
2129 2176 if gr is None:
2130 2177 break
2131 2178 if cnt == parents_recursion_limit:
2132 2179 # this will prevent accidental infinit loops
2133 2180 log.error(('more than %s parents found for group %s, stopping '
2134 2181 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 2182 break
2136 2183
2137 2184 groups.insert(0, gr)
2138 2185 return groups
2139 2186
2140 2187 @property
2141 2188 def children(self):
2142 2189 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143 2190
2144 2191 @property
2145 2192 def name(self):
2146 2193 return self.group_name.split(RepoGroup.url_sep())[-1]
2147 2194
2148 2195 @property
2149 2196 def full_path(self):
2150 2197 return self.group_name
2151 2198
2152 2199 @property
2153 2200 def full_path_splitted(self):
2154 2201 return self.group_name.split(RepoGroup.url_sep())
2155 2202
2156 2203 @property
2157 2204 def repositories(self):
2158 2205 return Repository.query()\
2159 2206 .filter(Repository.group == self)\
2160 2207 .order_by(Repository.repo_name)
2161 2208
2162 2209 @property
2163 2210 def repositories_recursive_count(self):
2164 2211 cnt = self.repositories.count()
2165 2212
2166 2213 def children_count(group):
2167 2214 cnt = 0
2168 2215 for child in group.children:
2169 2216 cnt += child.repositories.count()
2170 2217 cnt += children_count(child)
2171 2218 return cnt
2172 2219
2173 2220 return cnt + children_count(self)
2174 2221
2175 2222 def _recursive_objects(self, include_repos=True):
2176 2223 all_ = []
2177 2224
2178 2225 def _get_members(root_gr):
2179 2226 if include_repos:
2180 2227 for r in root_gr.repositories:
2181 2228 all_.append(r)
2182 2229 childs = root_gr.children.all()
2183 2230 if childs:
2184 2231 for gr in childs:
2185 2232 all_.append(gr)
2186 2233 _get_members(gr)
2187 2234
2188 2235 _get_members(self)
2189 2236 return [self] + all_
2190 2237
2191 2238 def recursive_groups_and_repos(self):
2192 2239 """
2193 2240 Recursive return all groups, with repositories in those groups
2194 2241 """
2195 2242 return self._recursive_objects()
2196 2243
2197 2244 def recursive_groups(self):
2198 2245 """
2199 2246 Returns all children groups for this group including children of children
2200 2247 """
2201 2248 return self._recursive_objects(include_repos=False)
2202 2249
2203 2250 def get_new_name(self, group_name):
2204 2251 """
2205 2252 returns new full group name based on parent and new name
2206 2253
2207 2254 :param group_name:
2208 2255 """
2209 2256 path_prefix = (self.parent_group.full_path_splitted if
2210 2257 self.parent_group else [])
2211 2258 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212 2259
2213 2260 def permissions(self, with_admins=True, with_owner=True):
2214 2261 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 2262 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 2263 joinedload(UserRepoGroupToPerm.user),
2217 2264 joinedload(UserRepoGroupToPerm.permission),)
2218 2265
2219 2266 # get owners and admins and permissions. We do a trick of re-writing
2220 2267 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 2268 # has a global reference and changing one object propagates to all
2222 2269 # others. This means if admin is also an owner admin_row that change
2223 2270 # would propagate to both objects
2224 2271 perm_rows = []
2225 2272 for _usr in q.all():
2226 2273 usr = AttributeDict(_usr.user.get_dict())
2227 2274 usr.permission = _usr.permission.permission_name
2228 2275 perm_rows.append(usr)
2229 2276
2230 2277 # filter the perm rows by 'default' first and then sort them by
2231 2278 # admin,write,read,none permissions sorted again alphabetically in
2232 2279 # each group
2233 2280 perm_rows = sorted(perm_rows, key=display_sort)
2234 2281
2235 2282 _admin_perm = 'group.admin'
2236 2283 owner_row = []
2237 2284 if with_owner:
2238 2285 usr = AttributeDict(self.user.get_dict())
2239 2286 usr.owner_row = True
2240 2287 usr.permission = _admin_perm
2241 2288 owner_row.append(usr)
2242 2289
2243 2290 super_admin_rows = []
2244 2291 if with_admins:
2245 2292 for usr in User.get_all_super_admins():
2246 2293 # if this admin is also owner, don't double the record
2247 2294 if usr.user_id == owner_row[0].user_id:
2248 2295 owner_row[0].admin_row = True
2249 2296 else:
2250 2297 usr = AttributeDict(usr.get_dict())
2251 2298 usr.admin_row = True
2252 2299 usr.permission = _admin_perm
2253 2300 super_admin_rows.append(usr)
2254 2301
2255 2302 return super_admin_rows + owner_row + perm_rows
2256 2303
2257 2304 def permission_user_groups(self):
2258 2305 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 2306 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 2307 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 2308 joinedload(UserGroupRepoGroupToPerm.permission),)
2262 2309
2263 2310 perm_rows = []
2264 2311 for _user_group in q.all():
2265 2312 usr = AttributeDict(_user_group.users_group.get_dict())
2266 2313 usr.permission = _user_group.permission.permission_name
2267 2314 perm_rows.append(usr)
2268 2315
2269 2316 return perm_rows
2270 2317
2271 2318 def get_api_data(self):
2272 2319 """
2273 2320 Common function for generating api data
2274 2321
2275 2322 """
2276 2323 group = self
2277 2324 data = {
2278 2325 'group_id': group.group_id,
2279 2326 'group_name': group.group_name,
2280 2327 'group_description': group.group_description,
2281 2328 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 2329 'repositories': [x.repo_name for x in group.repositories],
2283 2330 'owner': group.user.username,
2284 2331 }
2285 2332 return data
2286 2333
2287 2334
2288 2335 class Permission(Base, BaseModel):
2289 2336 __tablename__ = 'permissions'
2290 2337 __table_args__ = (
2291 2338 Index('p_perm_name_idx', 'permission_name'),
2292 2339 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 2340 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 2341 )
2295 2342 PERMS = [
2296 2343 ('hg.admin', _('RhodeCode Super Administrator')),
2297 2344
2298 2345 ('repository.none', _('Repository no access')),
2299 2346 ('repository.read', _('Repository read access')),
2300 2347 ('repository.write', _('Repository write access')),
2301 2348 ('repository.admin', _('Repository admin access')),
2302 2349
2303 2350 ('group.none', _('Repository group no access')),
2304 2351 ('group.read', _('Repository group read access')),
2305 2352 ('group.write', _('Repository group write access')),
2306 2353 ('group.admin', _('Repository group admin access')),
2307 2354
2308 2355 ('usergroup.none', _('User group no access')),
2309 2356 ('usergroup.read', _('User group read access')),
2310 2357 ('usergroup.write', _('User group write access')),
2311 2358 ('usergroup.admin', _('User group admin access')),
2312 2359
2313 2360 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 2361 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315 2362
2316 2363 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 2364 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318 2365
2319 2366 ('hg.create.none', _('Repository creation disabled')),
2320 2367 ('hg.create.repository', _('Repository creation enabled')),
2321 2368 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 2369 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323 2370
2324 2371 ('hg.fork.none', _('Repository forking disabled')),
2325 2372 ('hg.fork.repository', _('Repository forking enabled')),
2326 2373
2327 2374 ('hg.register.none', _('Registration disabled')),
2328 2375 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 2376 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330 2377
2331 2378 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 2379 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 2380 ('hg.password_reset.disabled', _('Password reset disabled')),
2334 2381
2335 2382 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 2383 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337 2384
2338 2385 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 2386 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 2387 ]
2341 2388
2342 2389 # definition of system default permissions for DEFAULT user
2343 2390 DEFAULT_USER_PERMISSIONS = [
2344 2391 'repository.read',
2345 2392 'group.read',
2346 2393 'usergroup.read',
2347 2394 'hg.create.repository',
2348 2395 'hg.repogroup.create.false',
2349 2396 'hg.usergroup.create.false',
2350 2397 'hg.create.write_on_repogroup.true',
2351 2398 'hg.fork.repository',
2352 2399 'hg.register.manual_activate',
2353 2400 'hg.password_reset.enabled',
2354 2401 'hg.extern_activate.auto',
2355 2402 'hg.inherit_default_perms.true',
2356 2403 ]
2357 2404
2358 2405 # defines which permissions are more important higher the more important
2359 2406 # Weight defines which permissions are more important.
2360 2407 # The higher number the more important.
2361 2408 PERM_WEIGHTS = {
2362 2409 'repository.none': 0,
2363 2410 'repository.read': 1,
2364 2411 'repository.write': 3,
2365 2412 'repository.admin': 4,
2366 2413
2367 2414 'group.none': 0,
2368 2415 'group.read': 1,
2369 2416 'group.write': 3,
2370 2417 'group.admin': 4,
2371 2418
2372 2419 'usergroup.none': 0,
2373 2420 'usergroup.read': 1,
2374 2421 'usergroup.write': 3,
2375 2422 'usergroup.admin': 4,
2376 2423
2377 2424 'hg.repogroup.create.false': 0,
2378 2425 'hg.repogroup.create.true': 1,
2379 2426
2380 2427 'hg.usergroup.create.false': 0,
2381 2428 'hg.usergroup.create.true': 1,
2382 2429
2383 2430 'hg.fork.none': 0,
2384 2431 'hg.fork.repository': 1,
2385 2432 'hg.create.none': 0,
2386 2433 'hg.create.repository': 1
2387 2434 }
2388 2435
2389 2436 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 2437 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 2438 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392 2439
2393 2440 def __unicode__(self):
2394 2441 return u"<%s('%s:%s')>" % (
2395 2442 self.__class__.__name__, self.permission_id, self.permission_name
2396 2443 )
2397 2444
2398 2445 @classmethod
2399 2446 def get_by_key(cls, key):
2400 2447 return cls.query().filter(cls.permission_name == key).scalar()
2401 2448
2402 2449 @classmethod
2403 2450 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 2451 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 2452 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 2453 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 2454 .filter(UserRepoToPerm.user_id == user_id)
2408 2455 if repo_id:
2409 2456 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 2457 return q.all()
2411 2458
2412 2459 @classmethod
2413 2460 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 2461 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 2462 .join(
2416 2463 Permission,
2417 2464 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 2465 .join(
2419 2466 Repository,
2420 2467 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 2468 .join(
2422 2469 UserGroup,
2423 2470 UserGroupRepoToPerm.users_group_id ==
2424 2471 UserGroup.users_group_id)\
2425 2472 .join(
2426 2473 UserGroupMember,
2427 2474 UserGroupRepoToPerm.users_group_id ==
2428 2475 UserGroupMember.users_group_id)\
2429 2476 .filter(
2430 2477 UserGroupMember.user_id == user_id,
2431 2478 UserGroup.users_group_active == true())
2432 2479 if repo_id:
2433 2480 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 2481 return q.all()
2435 2482
2436 2483 @classmethod
2437 2484 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 2485 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 2486 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 2487 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 2488 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 2489 if repo_group_id:
2443 2490 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 2491 return q.all()
2445 2492
2446 2493 @classmethod
2447 2494 def get_default_group_perms_from_user_group(
2448 2495 cls, user_id, repo_group_id=None):
2449 2496 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 2497 .join(
2451 2498 Permission,
2452 2499 UserGroupRepoGroupToPerm.permission_id ==
2453 2500 Permission.permission_id)\
2454 2501 .join(
2455 2502 RepoGroup,
2456 2503 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 2504 .join(
2458 2505 UserGroup,
2459 2506 UserGroupRepoGroupToPerm.users_group_id ==
2460 2507 UserGroup.users_group_id)\
2461 2508 .join(
2462 2509 UserGroupMember,
2463 2510 UserGroupRepoGroupToPerm.users_group_id ==
2464 2511 UserGroupMember.users_group_id)\
2465 2512 .filter(
2466 2513 UserGroupMember.user_id == user_id,
2467 2514 UserGroup.users_group_active == true())
2468 2515 if repo_group_id:
2469 2516 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 2517 return q.all()
2471 2518
2472 2519 @classmethod
2473 2520 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 2521 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 2522 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 2523 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 2524 .filter(UserUserGroupToPerm.user_id == user_id)
2478 2525 if user_group_id:
2479 2526 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 2527 return q.all()
2481 2528
2482 2529 @classmethod
2483 2530 def get_default_user_group_perms_from_user_group(
2484 2531 cls, user_id, user_group_id=None):
2485 2532 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 2533 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 2534 .join(
2488 2535 Permission,
2489 2536 UserGroupUserGroupToPerm.permission_id ==
2490 2537 Permission.permission_id)\
2491 2538 .join(
2492 2539 TargetUserGroup,
2493 2540 UserGroupUserGroupToPerm.target_user_group_id ==
2494 2541 TargetUserGroup.users_group_id)\
2495 2542 .join(
2496 2543 UserGroup,
2497 2544 UserGroupUserGroupToPerm.user_group_id ==
2498 2545 UserGroup.users_group_id)\
2499 2546 .join(
2500 2547 UserGroupMember,
2501 2548 UserGroupUserGroupToPerm.user_group_id ==
2502 2549 UserGroupMember.users_group_id)\
2503 2550 .filter(
2504 2551 UserGroupMember.user_id == user_id,
2505 2552 UserGroup.users_group_active == true())
2506 2553 if user_group_id:
2507 2554 q = q.filter(
2508 2555 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509 2556
2510 2557 return q.all()
2511 2558
2512 2559
2513 2560 class UserRepoToPerm(Base, BaseModel):
2514 2561 __tablename__ = 'repo_to_perm'
2515 2562 __table_args__ = (
2516 2563 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 2564 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 2565 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 2566 )
2520 2567 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 2568 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 2569 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 2570 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524 2571
2525 2572 user = relationship('User')
2526 2573 repository = relationship('Repository')
2527 2574 permission = relationship('Permission')
2528 2575
2529 2576 @classmethod
2530 2577 def create(cls, user, repository, permission):
2531 2578 n = cls()
2532 2579 n.user = user
2533 2580 n.repository = repository
2534 2581 n.permission = permission
2535 2582 Session().add(n)
2536 2583 return n
2537 2584
2538 2585 def __unicode__(self):
2539 2586 return u'<%s => %s >' % (self.user, self.repository)
2540 2587
2541 2588
2542 2589 class UserUserGroupToPerm(Base, BaseModel):
2543 2590 __tablename__ = 'user_user_group_to_perm'
2544 2591 __table_args__ = (
2545 2592 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 2595 )
2549 2596 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2550 2597 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 2599 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553 2600
2554 2601 user = relationship('User')
2555 2602 user_group = relationship('UserGroup')
2556 2603 permission = relationship('Permission')
2557 2604
2558 2605 @classmethod
2559 2606 def create(cls, user, user_group, permission):
2560 2607 n = cls()
2561 2608 n.user = user
2562 2609 n.user_group = user_group
2563 2610 n.permission = permission
2564 2611 Session().add(n)
2565 2612 return n
2566 2613
2567 2614 def __unicode__(self):
2568 2615 return u'<%s => %s >' % (self.user, self.user_group)
2569 2616
2570 2617
2571 2618 class UserToPerm(Base, BaseModel):
2572 2619 __tablename__ = 'user_to_perm'
2573 2620 __table_args__ = (
2574 2621 UniqueConstraint('user_id', 'permission_id'),
2575 2622 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2623 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2624 )
2578 2625 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2626 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 2627 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2628
2582 2629 user = relationship('User')
2583 2630 permission = relationship('Permission', lazy='joined')
2584 2631
2585 2632 def __unicode__(self):
2586 2633 return u'<%s => %s >' % (self.user, self.permission)
2587 2634
2588 2635
2589 2636 class UserGroupRepoToPerm(Base, BaseModel):
2590 2637 __tablename__ = 'users_group_repo_to_perm'
2591 2638 __table_args__ = (
2592 2639 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 2640 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 2641 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 2642 )
2596 2643 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 2644 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 2645 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 2646 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600 2647
2601 2648 users_group = relationship('UserGroup')
2602 2649 permission = relationship('Permission')
2603 2650 repository = relationship('Repository')
2604 2651
2605 2652 @classmethod
2606 2653 def create(cls, users_group, repository, permission):
2607 2654 n = cls()
2608 2655 n.users_group = users_group
2609 2656 n.repository = repository
2610 2657 n.permission = permission
2611 2658 Session().add(n)
2612 2659 return n
2613 2660
2614 2661 def __unicode__(self):
2615 2662 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616 2663
2617 2664
2618 2665 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 2666 __tablename__ = 'user_group_user_group_to_perm'
2620 2667 __table_args__ = (
2621 2668 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 2669 CheckConstraint('target_user_group_id != user_group_id'),
2623 2670 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 2671 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 2672 )
2626 2673 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)
2627 2674 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 2675 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 2676 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630 2677
2631 2678 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 2679 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 2680 permission = relationship('Permission')
2634 2681
2635 2682 @classmethod
2636 2683 def create(cls, target_user_group, user_group, permission):
2637 2684 n = cls()
2638 2685 n.target_user_group = target_user_group
2639 2686 n.user_group = user_group
2640 2687 n.permission = permission
2641 2688 Session().add(n)
2642 2689 return n
2643 2690
2644 2691 def __unicode__(self):
2645 2692 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646 2693
2647 2694
2648 2695 class UserGroupToPerm(Base, BaseModel):
2649 2696 __tablename__ = 'users_group_to_perm'
2650 2697 __table_args__ = (
2651 2698 UniqueConstraint('users_group_id', 'permission_id',),
2652 2699 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 2700 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 2701 )
2655 2702 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 2703 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 2704 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658 2705
2659 2706 users_group = relationship('UserGroup')
2660 2707 permission = relationship('Permission')
2661 2708
2662 2709
2663 2710 class UserRepoGroupToPerm(Base, BaseModel):
2664 2711 __tablename__ = 'user_repo_group_to_perm'
2665 2712 __table_args__ = (
2666 2713 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 2714 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 2715 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 2716 )
2670 2717
2671 2718 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 2719 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 2720 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 2721 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675 2722
2676 2723 user = relationship('User')
2677 2724 group = relationship('RepoGroup')
2678 2725 permission = relationship('Permission')
2679 2726
2680 2727 @classmethod
2681 2728 def create(cls, user, repository_group, permission):
2682 2729 n = cls()
2683 2730 n.user = user
2684 2731 n.group = repository_group
2685 2732 n.permission = permission
2686 2733 Session().add(n)
2687 2734 return n
2688 2735
2689 2736
2690 2737 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 2738 __tablename__ = 'users_group_repo_group_to_perm'
2692 2739 __table_args__ = (
2693 2740 UniqueConstraint('users_group_id', 'group_id'),
2694 2741 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 2742 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 2743 )
2697 2744
2698 2745 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)
2699 2746 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 2747 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 2748 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702 2749
2703 2750 users_group = relationship('UserGroup')
2704 2751 permission = relationship('Permission')
2705 2752 group = relationship('RepoGroup')
2706 2753
2707 2754 @classmethod
2708 2755 def create(cls, user_group, repository_group, permission):
2709 2756 n = cls()
2710 2757 n.users_group = user_group
2711 2758 n.group = repository_group
2712 2759 n.permission = permission
2713 2760 Session().add(n)
2714 2761 return n
2715 2762
2716 2763 def __unicode__(self):
2717 2764 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718 2765
2719 2766
2720 2767 class Statistics(Base, BaseModel):
2721 2768 __tablename__ = 'statistics'
2722 2769 __table_args__ = (
2723 2770 UniqueConstraint('repository_id'),
2724 2771 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2772 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2773 )
2727 2774 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 2775 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 2776 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 2777 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 2778 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 2779 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733 2780
2734 2781 repository = relationship('Repository', single_parent=True)
2735 2782
2736 2783
2737 2784 class UserFollowing(Base, BaseModel):
2738 2785 __tablename__ = 'user_followings'
2739 2786 __table_args__ = (
2740 2787 UniqueConstraint('user_id', 'follows_repository_id'),
2741 2788 UniqueConstraint('user_id', 'follows_user_id'),
2742 2789 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 2790 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 2791 )
2745 2792
2746 2793 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 2794 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 2795 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 2796 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 2797 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751 2798
2752 2799 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753 2800
2754 2801 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 2802 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756 2803
2757 2804 @classmethod
2758 2805 def get_repo_followers(cls, repo_id):
2759 2806 return cls.query().filter(cls.follows_repo_id == repo_id)
2760 2807
2761 2808
2762 2809 class CacheKey(Base, BaseModel):
2763 2810 __tablename__ = 'cache_invalidation'
2764 2811 __table_args__ = (
2765 2812 UniqueConstraint('cache_key'),
2766 2813 Index('key_idx', 'cache_key'),
2767 2814 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 2815 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 2816 )
2770 2817 CACHE_TYPE_ATOM = 'ATOM'
2771 2818 CACHE_TYPE_RSS = 'RSS'
2772 2819 CACHE_TYPE_README = 'README'
2773 2820
2774 2821 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 2822 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 2823 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 2824 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778 2825
2779 2826 def __init__(self, cache_key, cache_args=''):
2780 2827 self.cache_key = cache_key
2781 2828 self.cache_args = cache_args
2782 2829 self.cache_active = False
2783 2830
2784 2831 def __unicode__(self):
2785 2832 return u"<%s('%s:%s[%s]')>" % (
2786 2833 self.__class__.__name__,
2787 2834 self.cache_id, self.cache_key, self.cache_active)
2788 2835
2789 2836 def _cache_key_partition(self):
2790 2837 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 2838 return prefix, repo_name, suffix
2792 2839
2793 2840 def get_prefix(self):
2794 2841 """
2795 2842 Try to extract prefix from existing cache key. The key could consist
2796 2843 of prefix, repo_name, suffix
2797 2844 """
2798 2845 # this returns prefix, repo_name, suffix
2799 2846 return self._cache_key_partition()[0]
2800 2847
2801 2848 def get_suffix(self):
2802 2849 """
2803 2850 get suffix that might have been used in _get_cache_key to
2804 2851 generate self.cache_key. Only used for informational purposes
2805 2852 in repo_edit.mako.
2806 2853 """
2807 2854 # prefix, repo_name, suffix
2808 2855 return self._cache_key_partition()[2]
2809 2856
2810 2857 @classmethod
2811 2858 def delete_all_cache(cls):
2812 2859 """
2813 2860 Delete all cache keys from database.
2814 2861 Should only be run when all instances are down and all entries
2815 2862 thus stale.
2816 2863 """
2817 2864 cls.query().delete()
2818 2865 Session().commit()
2819 2866
2820 2867 @classmethod
2821 2868 def get_cache_key(cls, repo_name, cache_type):
2822 2869 """
2823 2870
2824 2871 Generate a cache key for this process of RhodeCode instance.
2825 2872 Prefix most likely will be process id or maybe explicitly set
2826 2873 instance_id from .ini file.
2827 2874 """
2828 2875 import rhodecode
2829 2876 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830 2877
2831 2878 repo_as_unicode = safe_unicode(repo_name)
2832 2879 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 2880 if cache_type else repo_as_unicode
2834 2881
2835 2882 return u'{}{}'.format(prefix, key)
2836 2883
2837 2884 @classmethod
2838 2885 def set_invalidate(cls, repo_name, delete=False):
2839 2886 """
2840 2887 Mark all caches of a repo as invalid in the database.
2841 2888 """
2842 2889
2843 2890 try:
2844 2891 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 2892 if delete:
2846 2893 log.debug('cache objects deleted for repo %s',
2847 2894 safe_str(repo_name))
2848 2895 qry.delete()
2849 2896 else:
2850 2897 log.debug('cache objects marked as invalid for repo %s',
2851 2898 safe_str(repo_name))
2852 2899 qry.update({"cache_active": False})
2853 2900
2854 2901 Session().commit()
2855 2902 except Exception:
2856 2903 log.exception(
2857 2904 'Cache key invalidation failed for repository %s',
2858 2905 safe_str(repo_name))
2859 2906 Session().rollback()
2860 2907
2861 2908 @classmethod
2862 2909 def get_active_cache(cls, cache_key):
2863 2910 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 2911 if inv_obj:
2865 2912 return inv_obj
2866 2913 return None
2867 2914
2868 2915 @classmethod
2869 2916 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 2917 thread_scoped=False):
2871 2918 """
2872 2919 @cache_region('long_term')
2873 2920 def _heavy_calculation(cache_key):
2874 2921 return 'result'
2875 2922
2876 2923 cache_context = CacheKey.repo_context_cache(
2877 2924 _heavy_calculation, repo_name, cache_type)
2878 2925
2879 2926 with cache_context as context:
2880 2927 context.invalidate()
2881 2928 computed = context.compute()
2882 2929
2883 2930 assert computed == 'result'
2884 2931 """
2885 2932 from rhodecode.lib import caches
2886 2933 return caches.InvalidationContext(
2887 2934 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888 2935
2889 2936
2890 2937 class ChangesetComment(Base, BaseModel):
2891 2938 __tablename__ = 'changeset_comments'
2892 2939 __table_args__ = (
2893 2940 Index('cc_revision_idx', 'revision'),
2894 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 2943 )
2897 2944
2898 2945 COMMENT_OUTDATED = u'comment_outdated'
2899 2946 COMMENT_TYPE_NOTE = u'note'
2900 2947 COMMENT_TYPE_TODO = u'todo'
2901 2948 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2902 2949
2903 2950 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2904 2951 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2905 2952 revision = Column('revision', String(40), nullable=True)
2906 2953 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2907 2954 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2908 2955 line_no = Column('line_no', Unicode(10), nullable=True)
2909 2956 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2910 2957 f_path = Column('f_path', Unicode(1000), nullable=True)
2911 2958 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2912 2959 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2913 2960 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 2961 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2915 2962 renderer = Column('renderer', Unicode(64), nullable=True)
2916 2963 display_state = Column('display_state', Unicode(128), nullable=True)
2917 2964
2918 2965 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2919 2966 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2920 2967 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2921 2968 author = relationship('User', lazy='joined')
2922 2969 repo = relationship('Repository')
2923 2970 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
2924 2971 pull_request = relationship('PullRequest', lazy='joined')
2925 2972 pull_request_version = relationship('PullRequestVersion')
2926 2973
2927 2974 @classmethod
2928 2975 def get_users(cls, revision=None, pull_request_id=None):
2929 2976 """
2930 2977 Returns user associated with this ChangesetComment. ie those
2931 2978 who actually commented
2932 2979
2933 2980 :param cls:
2934 2981 :param revision:
2935 2982 """
2936 2983 q = Session().query(User)\
2937 2984 .join(ChangesetComment.author)
2938 2985 if revision:
2939 2986 q = q.filter(cls.revision == revision)
2940 2987 elif pull_request_id:
2941 2988 q = q.filter(cls.pull_request_id == pull_request_id)
2942 2989 return q.all()
2943 2990
2944 2991 @classmethod
2945 2992 def get_index_from_version(cls, pr_version, versions):
2946 2993 num_versions = [x.pull_request_version_id for x in versions]
2947 2994 try:
2948 2995 return num_versions.index(pr_version) +1
2949 2996 except (IndexError, ValueError):
2950 2997 return
2951 2998
2952 2999 @property
2953 3000 def outdated(self):
2954 3001 return self.display_state == self.COMMENT_OUTDATED
2955 3002
2956 3003 def outdated_at_version(self, version):
2957 3004 """
2958 3005 Checks if comment is outdated for given pull request version
2959 3006 """
2960 3007 return self.outdated and self.pull_request_version_id != version
2961 3008
2962 3009 def older_than_version(self, version):
2963 3010 """
2964 3011 Checks if comment is made from previous version than given
2965 3012 """
2966 3013 if version is None:
2967 3014 return self.pull_request_version_id is not None
2968 3015
2969 3016 return self.pull_request_version_id < version
2970 3017
2971 3018 @property
2972 3019 def resolved(self):
2973 3020 return self.resolved_by[0] if self.resolved_by else None
2974 3021
2975 3022 @property
2976 3023 def is_todo(self):
2977 3024 return self.comment_type == self.COMMENT_TYPE_TODO
2978 3025
2979 3026 def get_index_version(self, versions):
2980 3027 return self.get_index_from_version(
2981 3028 self.pull_request_version_id, versions)
2982 3029
2983 3030 def render(self, mentions=False):
2984 3031 from rhodecode.lib import helpers as h
2985 3032 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2986 3033
2987 3034 def __repr__(self):
2988 3035 if self.comment_id:
2989 3036 return '<DB:Comment #%s>' % self.comment_id
2990 3037 else:
2991 3038 return '<DB:Comment at %#x>' % id(self)
2992 3039
2993 3040
2994 3041 class ChangesetStatus(Base, BaseModel):
2995 3042 __tablename__ = 'changeset_statuses'
2996 3043 __table_args__ = (
2997 3044 Index('cs_revision_idx', 'revision'),
2998 3045 Index('cs_version_idx', 'version'),
2999 3046 UniqueConstraint('repo_id', 'revision', 'version'),
3000 3047 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3001 3048 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3002 3049 )
3003 3050 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3004 3051 STATUS_APPROVED = 'approved'
3005 3052 STATUS_REJECTED = 'rejected'
3006 3053 STATUS_UNDER_REVIEW = 'under_review'
3007 3054
3008 3055 STATUSES = [
3009 3056 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3010 3057 (STATUS_APPROVED, _("Approved")),
3011 3058 (STATUS_REJECTED, _("Rejected")),
3012 3059 (STATUS_UNDER_REVIEW, _("Under Review")),
3013 3060 ]
3014 3061
3015 3062 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3016 3063 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3017 3064 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3018 3065 revision = Column('revision', String(40), nullable=False)
3019 3066 status = Column('status', String(128), nullable=False, default=DEFAULT)
3020 3067 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3021 3068 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3022 3069 version = Column('version', Integer(), nullable=False, default=0)
3023 3070 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3024 3071
3025 3072 author = relationship('User', lazy='joined')
3026 3073 repo = relationship('Repository')
3027 3074 comment = relationship('ChangesetComment', lazy='joined')
3028 3075 pull_request = relationship('PullRequest', lazy='joined')
3029 3076
3030 3077 def __unicode__(self):
3031 3078 return u"<%s('%s[v%s]:%s')>" % (
3032 3079 self.__class__.__name__,
3033 3080 self.status, self.version, self.author
3034 3081 )
3035 3082
3036 3083 @classmethod
3037 3084 def get_status_lbl(cls, value):
3038 3085 return dict(cls.STATUSES).get(value)
3039 3086
3040 3087 @property
3041 3088 def status_lbl(self):
3042 3089 return ChangesetStatus.get_status_lbl(self.status)
3043 3090
3044 3091
3045 3092 class _PullRequestBase(BaseModel):
3046 3093 """
3047 3094 Common attributes of pull request and version entries.
3048 3095 """
3049 3096
3050 3097 # .status values
3051 3098 STATUS_NEW = u'new'
3052 3099 STATUS_OPEN = u'open'
3053 3100 STATUS_CLOSED = u'closed'
3054 3101
3055 3102 title = Column('title', Unicode(255), nullable=True)
3056 3103 description = Column(
3057 3104 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3058 3105 nullable=True)
3059 3106 # new/open/closed status of pull request (not approve/reject/etc)
3060 3107 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3061 3108 created_on = Column(
3062 3109 'created_on', DateTime(timezone=False), nullable=False,
3063 3110 default=datetime.datetime.now)
3064 3111 updated_on = Column(
3065 3112 'updated_on', DateTime(timezone=False), nullable=False,
3066 3113 default=datetime.datetime.now)
3067 3114
3068 3115 @declared_attr
3069 3116 def user_id(cls):
3070 3117 return Column(
3071 3118 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3072 3119 unique=None)
3073 3120
3074 3121 # 500 revisions max
3075 3122 _revisions = Column(
3076 3123 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3077 3124
3078 3125 @declared_attr
3079 3126 def source_repo_id(cls):
3080 3127 # TODO: dan: rename column to source_repo_id
3081 3128 return Column(
3082 3129 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3083 3130 nullable=False)
3084 3131
3085 3132 source_ref = Column('org_ref', Unicode(255), nullable=False)
3086 3133
3087 3134 @declared_attr
3088 3135 def target_repo_id(cls):
3089 3136 # TODO: dan: rename column to target_repo_id
3090 3137 return Column(
3091 3138 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3092 3139 nullable=False)
3093 3140
3094 3141 target_ref = Column('other_ref', Unicode(255), nullable=False)
3095 3142 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3096 3143
3097 3144 # TODO: dan: rename column to last_merge_source_rev
3098 3145 _last_merge_source_rev = Column(
3099 3146 'last_merge_org_rev', String(40), nullable=True)
3100 3147 # TODO: dan: rename column to last_merge_target_rev
3101 3148 _last_merge_target_rev = Column(
3102 3149 'last_merge_other_rev', String(40), nullable=True)
3103 3150 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3104 3151 merge_rev = Column('merge_rev', String(40), nullable=True)
3105 3152
3106 3153 @hybrid_property
3107 3154 def revisions(self):
3108 3155 return self._revisions.split(':') if self._revisions else []
3109 3156
3110 3157 @revisions.setter
3111 3158 def revisions(self, val):
3112 3159 self._revisions = ':'.join(val)
3113 3160
3114 3161 @declared_attr
3115 3162 def author(cls):
3116 3163 return relationship('User', lazy='joined')
3117 3164
3118 3165 @declared_attr
3119 3166 def source_repo(cls):
3120 3167 return relationship(
3121 3168 'Repository',
3122 3169 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3123 3170
3124 3171 @property
3125 3172 def source_ref_parts(self):
3126 3173 return self.unicode_to_reference(self.source_ref)
3127 3174
3128 3175 @declared_attr
3129 3176 def target_repo(cls):
3130 3177 return relationship(
3131 3178 'Repository',
3132 3179 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3133 3180
3134 3181 @property
3135 3182 def target_ref_parts(self):
3136 3183 return self.unicode_to_reference(self.target_ref)
3137 3184
3138 3185 @property
3139 3186 def shadow_merge_ref(self):
3140 3187 return self.unicode_to_reference(self._shadow_merge_ref)
3141 3188
3142 3189 @shadow_merge_ref.setter
3143 3190 def shadow_merge_ref(self, ref):
3144 3191 self._shadow_merge_ref = self.reference_to_unicode(ref)
3145 3192
3146 3193 def unicode_to_reference(self, raw):
3147 3194 """
3148 3195 Convert a unicode (or string) to a reference object.
3149 3196 If unicode evaluates to False it returns None.
3150 3197 """
3151 3198 if raw:
3152 3199 refs = raw.split(':')
3153 3200 return Reference(*refs)
3154 3201 else:
3155 3202 return None
3156 3203
3157 3204 def reference_to_unicode(self, ref):
3158 3205 """
3159 3206 Convert a reference object to unicode.
3160 3207 If reference is None it returns None.
3161 3208 """
3162 3209 if ref:
3163 3210 return u':'.join(ref)
3164 3211 else:
3165 3212 return None
3166 3213
3167 3214 def get_api_data(self):
3168 3215 from rhodecode.model.pull_request import PullRequestModel
3169 3216 pull_request = self
3170 3217 merge_status = PullRequestModel().merge_status(pull_request)
3171 3218
3172 3219 pull_request_url = url(
3173 3220 'pullrequest_show', repo_name=self.target_repo.repo_name,
3174 3221 pull_request_id=self.pull_request_id, qualified=True)
3175 3222
3176 3223 merge_data = {
3177 3224 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3178 3225 'reference': (
3179 3226 pull_request.shadow_merge_ref._asdict()
3180 3227 if pull_request.shadow_merge_ref else None),
3181 3228 }
3182 3229
3183 3230 data = {
3184 3231 'pull_request_id': pull_request.pull_request_id,
3185 3232 'url': pull_request_url,
3186 3233 'title': pull_request.title,
3187 3234 'description': pull_request.description,
3188 3235 'status': pull_request.status,
3189 3236 'created_on': pull_request.created_on,
3190 3237 'updated_on': pull_request.updated_on,
3191 3238 'commit_ids': pull_request.revisions,
3192 3239 'review_status': pull_request.calculated_review_status(),
3193 3240 'mergeable': {
3194 3241 'status': merge_status[0],
3195 3242 'message': unicode(merge_status[1]),
3196 3243 },
3197 3244 'source': {
3198 3245 'clone_url': pull_request.source_repo.clone_url(),
3199 3246 'repository': pull_request.source_repo.repo_name,
3200 3247 'reference': {
3201 3248 'name': pull_request.source_ref_parts.name,
3202 3249 'type': pull_request.source_ref_parts.type,
3203 3250 'commit_id': pull_request.source_ref_parts.commit_id,
3204 3251 },
3205 3252 },
3206 3253 'target': {
3207 3254 'clone_url': pull_request.target_repo.clone_url(),
3208 3255 'repository': pull_request.target_repo.repo_name,
3209 3256 'reference': {
3210 3257 'name': pull_request.target_ref_parts.name,
3211 3258 'type': pull_request.target_ref_parts.type,
3212 3259 'commit_id': pull_request.target_ref_parts.commit_id,
3213 3260 },
3214 3261 },
3215 3262 'merge': merge_data,
3216 3263 'author': pull_request.author.get_api_data(include_secrets=False,
3217 3264 details='basic'),
3218 3265 'reviewers': [
3219 3266 {
3220 3267 'user': reviewer.get_api_data(include_secrets=False,
3221 3268 details='basic'),
3222 3269 'reasons': reasons,
3223 3270 'review_status': st[0][1].status if st else 'not_reviewed',
3224 3271 }
3225 3272 for reviewer, reasons, st in pull_request.reviewers_statuses()
3226 3273 ]
3227 3274 }
3228 3275
3229 3276 return data
3230 3277
3231 3278
3232 3279 class PullRequest(Base, _PullRequestBase):
3233 3280 __tablename__ = 'pull_requests'
3234 3281 __table_args__ = (
3235 3282 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3236 3283 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3237 3284 )
3238 3285
3239 3286 pull_request_id = Column(
3240 3287 'pull_request_id', Integer(), nullable=False, primary_key=True)
3241 3288
3242 3289 def __repr__(self):
3243 3290 if self.pull_request_id:
3244 3291 return '<DB:PullRequest #%s>' % self.pull_request_id
3245 3292 else:
3246 3293 return '<DB:PullRequest at %#x>' % id(self)
3247 3294
3248 3295 reviewers = relationship('PullRequestReviewers',
3249 3296 cascade="all, delete, delete-orphan")
3250 3297 statuses = relationship('ChangesetStatus')
3251 3298 comments = relationship('ChangesetComment',
3252 3299 cascade="all, delete, delete-orphan")
3253 3300 versions = relationship('PullRequestVersion',
3254 3301 cascade="all, delete, delete-orphan",
3255 3302 lazy='dynamic')
3256 3303
3257 3304 @classmethod
3258 3305 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3259 3306 internal_methods=None):
3260 3307
3261 3308 class PullRequestDisplay(object):
3262 3309 """
3263 3310 Special object wrapper for showing PullRequest data via Versions
3264 3311 It mimics PR object as close as possible. This is read only object
3265 3312 just for display
3266 3313 """
3267 3314
3268 3315 def __init__(self, attrs, internal=None):
3269 3316 self.attrs = attrs
3270 3317 # internal have priority over the given ones via attrs
3271 3318 self.internal = internal or ['versions']
3272 3319
3273 3320 def __getattr__(self, item):
3274 3321 if item in self.internal:
3275 3322 return getattr(self, item)
3276 3323 try:
3277 3324 return self.attrs[item]
3278 3325 except KeyError:
3279 3326 raise AttributeError(
3280 3327 '%s object has no attribute %s' % (self, item))
3281 3328
3282 3329 def __repr__(self):
3283 3330 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3284 3331
3285 3332 def versions(self):
3286 3333 return pull_request_obj.versions.order_by(
3287 3334 PullRequestVersion.pull_request_version_id).all()
3288 3335
3289 3336 def is_closed(self):
3290 3337 return pull_request_obj.is_closed()
3291 3338
3292 3339 @property
3293 3340 def pull_request_version_id(self):
3294 3341 return getattr(pull_request_obj, 'pull_request_version_id', None)
3295 3342
3296 3343 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3297 3344
3298 3345 attrs.author = StrictAttributeDict(
3299 3346 pull_request_obj.author.get_api_data())
3300 3347 if pull_request_obj.target_repo:
3301 3348 attrs.target_repo = StrictAttributeDict(
3302 3349 pull_request_obj.target_repo.get_api_data())
3303 3350 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3304 3351
3305 3352 if pull_request_obj.source_repo:
3306 3353 attrs.source_repo = StrictAttributeDict(
3307 3354 pull_request_obj.source_repo.get_api_data())
3308 3355 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3309 3356
3310 3357 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3311 3358 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3312 3359 attrs.revisions = pull_request_obj.revisions
3313 3360
3314 3361 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3315 3362
3316 3363 return PullRequestDisplay(attrs, internal=internal_methods)
3317 3364
3318 3365 def is_closed(self):
3319 3366 return self.status == self.STATUS_CLOSED
3320 3367
3321 3368 def __json__(self):
3322 3369 return {
3323 3370 'revisions': self.revisions,
3324 3371 }
3325 3372
3326 3373 def calculated_review_status(self):
3327 3374 from rhodecode.model.changeset_status import ChangesetStatusModel
3328 3375 return ChangesetStatusModel().calculated_review_status(self)
3329 3376
3330 3377 def reviewers_statuses(self):
3331 3378 from rhodecode.model.changeset_status import ChangesetStatusModel
3332 3379 return ChangesetStatusModel().reviewers_statuses(self)
3333 3380
3334 3381 @property
3335 3382 def workspace_id(self):
3336 3383 from rhodecode.model.pull_request import PullRequestModel
3337 3384 return PullRequestModel()._workspace_id(self)
3338 3385
3339 3386 def get_shadow_repo(self):
3340 3387 workspace_id = self.workspace_id
3341 3388 vcs_obj = self.target_repo.scm_instance()
3342 3389 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3343 3390 workspace_id)
3344 3391 return vcs_obj._get_shadow_instance(shadow_repository_path)
3345 3392
3346 3393
3347 3394 class PullRequestVersion(Base, _PullRequestBase):
3348 3395 __tablename__ = 'pull_request_versions'
3349 3396 __table_args__ = (
3350 3397 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3351 3398 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3352 3399 )
3353 3400
3354 3401 pull_request_version_id = Column(
3355 3402 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3356 3403 pull_request_id = Column(
3357 3404 'pull_request_id', Integer(),
3358 3405 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3359 3406 pull_request = relationship('PullRequest')
3360 3407
3361 3408 def __repr__(self):
3362 3409 if self.pull_request_version_id:
3363 3410 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3364 3411 else:
3365 3412 return '<DB:PullRequestVersion at %#x>' % id(self)
3366 3413
3367 3414 @property
3368 3415 def reviewers(self):
3369 3416 return self.pull_request.reviewers
3370 3417
3371 3418 @property
3372 3419 def versions(self):
3373 3420 return self.pull_request.versions
3374 3421
3375 3422 def is_closed(self):
3376 3423 # calculate from original
3377 3424 return self.pull_request.status == self.STATUS_CLOSED
3378 3425
3379 3426 def calculated_review_status(self):
3380 3427 return self.pull_request.calculated_review_status()
3381 3428
3382 3429 def reviewers_statuses(self):
3383 3430 return self.pull_request.reviewers_statuses()
3384 3431
3385 3432
3386 3433 class PullRequestReviewers(Base, BaseModel):
3387 3434 __tablename__ = 'pull_request_reviewers'
3388 3435 __table_args__ = (
3389 3436 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3390 3437 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3391 3438 )
3392 3439
3393 3440 def __init__(self, user=None, pull_request=None, reasons=None):
3394 3441 self.user = user
3395 3442 self.pull_request = pull_request
3396 3443 self.reasons = reasons or []
3397 3444
3398 3445 @hybrid_property
3399 3446 def reasons(self):
3400 3447 if not self._reasons:
3401 3448 return []
3402 3449 return self._reasons
3403 3450
3404 3451 @reasons.setter
3405 3452 def reasons(self, val):
3406 3453 val = val or []
3407 3454 if any(not isinstance(x, basestring) for x in val):
3408 3455 raise Exception('invalid reasons type, must be list of strings')
3409 3456 self._reasons = val
3410 3457
3411 3458 pull_requests_reviewers_id = Column(
3412 3459 'pull_requests_reviewers_id', Integer(), nullable=False,
3413 3460 primary_key=True)
3414 3461 pull_request_id = Column(
3415 3462 "pull_request_id", Integer(),
3416 3463 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3417 3464 user_id = Column(
3418 3465 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3419 3466 _reasons = Column(
3420 3467 'reason', MutationList.as_mutable(
3421 3468 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3422 3469
3423 3470 user = relationship('User')
3424 3471 pull_request = relationship('PullRequest')
3425 3472
3426 3473
3427 3474 class Notification(Base, BaseModel):
3428 3475 __tablename__ = 'notifications'
3429 3476 __table_args__ = (
3430 3477 Index('notification_type_idx', 'type'),
3431 3478 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3432 3479 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3433 3480 )
3434 3481
3435 3482 TYPE_CHANGESET_COMMENT = u'cs_comment'
3436 3483 TYPE_MESSAGE = u'message'
3437 3484 TYPE_MENTION = u'mention'
3438 3485 TYPE_REGISTRATION = u'registration'
3439 3486 TYPE_PULL_REQUEST = u'pull_request'
3440 3487 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3441 3488
3442 3489 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3443 3490 subject = Column('subject', Unicode(512), nullable=True)
3444 3491 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3445 3492 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3446 3493 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3447 3494 type_ = Column('type', Unicode(255))
3448 3495
3449 3496 created_by_user = relationship('User')
3450 3497 notifications_to_users = relationship('UserNotification', lazy='joined',
3451 3498 cascade="all, delete, delete-orphan")
3452 3499
3453 3500 @property
3454 3501 def recipients(self):
3455 3502 return [x.user for x in UserNotification.query()\
3456 3503 .filter(UserNotification.notification == self)\
3457 3504 .order_by(UserNotification.user_id.asc()).all()]
3458 3505
3459 3506 @classmethod
3460 3507 def create(cls, created_by, subject, body, recipients, type_=None):
3461 3508 if type_ is None:
3462 3509 type_ = Notification.TYPE_MESSAGE
3463 3510
3464 3511 notification = cls()
3465 3512 notification.created_by_user = created_by
3466 3513 notification.subject = subject
3467 3514 notification.body = body
3468 3515 notification.type_ = type_
3469 3516 notification.created_on = datetime.datetime.now()
3470 3517
3471 3518 for u in recipients:
3472 3519 assoc = UserNotification()
3473 3520 assoc.notification = notification
3474 3521
3475 3522 # if created_by is inside recipients mark his notification
3476 3523 # as read
3477 3524 if u.user_id == created_by.user_id:
3478 3525 assoc.read = True
3479 3526
3480 3527 u.notifications.append(assoc)
3481 3528 Session().add(notification)
3482 3529
3483 3530 return notification
3484 3531
3485 3532 @property
3486 3533 def description(self):
3487 3534 from rhodecode.model.notification import NotificationModel
3488 3535 return NotificationModel().make_description(self)
3489 3536
3490 3537
3491 3538 class UserNotification(Base, BaseModel):
3492 3539 __tablename__ = 'user_to_notification'
3493 3540 __table_args__ = (
3494 3541 UniqueConstraint('user_id', 'notification_id'),
3495 3542 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3496 3543 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3497 3544 )
3498 3545 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3499 3546 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3500 3547 read = Column('read', Boolean, default=False)
3501 3548 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3502 3549
3503 3550 user = relationship('User', lazy="joined")
3504 3551 notification = relationship('Notification', lazy="joined",
3505 3552 order_by=lambda: Notification.created_on.desc(),)
3506 3553
3507 3554 def mark_as_read(self):
3508 3555 self.read = True
3509 3556 Session().add(self)
3510 3557
3511 3558
3512 3559 class Gist(Base, BaseModel):
3513 3560 __tablename__ = 'gists'
3514 3561 __table_args__ = (
3515 3562 Index('g_gist_access_id_idx', 'gist_access_id'),
3516 3563 Index('g_created_on_idx', 'created_on'),
3517 3564 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3518 3565 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3519 3566 )
3520 3567 GIST_PUBLIC = u'public'
3521 3568 GIST_PRIVATE = u'private'
3522 3569 DEFAULT_FILENAME = u'gistfile1.txt'
3523 3570
3524 3571 ACL_LEVEL_PUBLIC = u'acl_public'
3525 3572 ACL_LEVEL_PRIVATE = u'acl_private'
3526 3573
3527 3574 gist_id = Column('gist_id', Integer(), primary_key=True)
3528 3575 gist_access_id = Column('gist_access_id', Unicode(250))
3529 3576 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3530 3577 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3531 3578 gist_expires = Column('gist_expires', Float(53), nullable=False)
3532 3579 gist_type = Column('gist_type', Unicode(128), nullable=False)
3533 3580 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3534 3581 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3535 3582 acl_level = Column('acl_level', Unicode(128), nullable=True)
3536 3583
3537 3584 owner = relationship('User')
3538 3585
3539 3586 def __repr__(self):
3540 3587 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3541 3588
3542 3589 @classmethod
3543 3590 def get_or_404(cls, id_):
3544 3591 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3545 3592 if not res:
3546 3593 raise HTTPNotFound
3547 3594 return res
3548 3595
3549 3596 @classmethod
3550 3597 def get_by_access_id(cls, gist_access_id):
3551 3598 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3552 3599
3553 3600 def gist_url(self):
3554 3601 import rhodecode
3555 3602 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3556 3603 if alias_url:
3557 3604 return alias_url.replace('{gistid}', self.gist_access_id)
3558 3605
3559 3606 return url('gist', gist_id=self.gist_access_id, qualified=True)
3560 3607
3561 3608 @classmethod
3562 3609 def base_path(cls):
3563 3610 """
3564 3611 Returns base path when all gists are stored
3565 3612
3566 3613 :param cls:
3567 3614 """
3568 3615 from rhodecode.model.gist import GIST_STORE_LOC
3569 3616 q = Session().query(RhodeCodeUi)\
3570 3617 .filter(RhodeCodeUi.ui_key == URL_SEP)
3571 3618 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3572 3619 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3573 3620
3574 3621 def get_api_data(self):
3575 3622 """
3576 3623 Common function for generating gist related data for API
3577 3624 """
3578 3625 gist = self
3579 3626 data = {
3580 3627 'gist_id': gist.gist_id,
3581 3628 'type': gist.gist_type,
3582 3629 'access_id': gist.gist_access_id,
3583 3630 'description': gist.gist_description,
3584 3631 'url': gist.gist_url(),
3585 3632 'expires': gist.gist_expires,
3586 3633 'created_on': gist.created_on,
3587 3634 'modified_at': gist.modified_at,
3588 3635 'content': None,
3589 3636 'acl_level': gist.acl_level,
3590 3637 }
3591 3638 return data
3592 3639
3593 3640 def __json__(self):
3594 3641 data = dict(
3595 3642 )
3596 3643 data.update(self.get_api_data())
3597 3644 return data
3598 3645 # SCM functions
3599 3646
3600 3647 def scm_instance(self, **kwargs):
3601 3648 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3602 3649 return get_vcs_instance(
3603 3650 repo_path=safe_str(full_repo_path), create=False)
3604 3651
3605 3652
3606 3653 class ExternalIdentity(Base, BaseModel):
3607 3654 __tablename__ = 'external_identities'
3608 3655 __table_args__ = (
3609 3656 Index('local_user_id_idx', 'local_user_id'),
3610 3657 Index('external_id_idx', 'external_id'),
3611 3658 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3612 3659 'mysql_charset': 'utf8'})
3613 3660
3614 3661 external_id = Column('external_id', Unicode(255), default=u'',
3615 3662 primary_key=True)
3616 3663 external_username = Column('external_username', Unicode(1024), default=u'')
3617 3664 local_user_id = Column('local_user_id', Integer(),
3618 3665 ForeignKey('users.user_id'), primary_key=True)
3619 3666 provider_name = Column('provider_name', Unicode(255), default=u'',
3620 3667 primary_key=True)
3621 3668 access_token = Column('access_token', String(1024), default=u'')
3622 3669 alt_token = Column('alt_token', String(1024), default=u'')
3623 3670 token_secret = Column('token_secret', String(1024), default=u'')
3624 3671
3625 3672 @classmethod
3626 3673 def by_external_id_and_provider(cls, external_id, provider_name,
3627 3674 local_user_id=None):
3628 3675 """
3629 3676 Returns ExternalIdentity instance based on search params
3630 3677
3631 3678 :param external_id:
3632 3679 :param provider_name:
3633 3680 :return: ExternalIdentity
3634 3681 """
3635 3682 query = cls.query()
3636 3683 query = query.filter(cls.external_id == external_id)
3637 3684 query = query.filter(cls.provider_name == provider_name)
3638 3685 if local_user_id:
3639 3686 query = query.filter(cls.local_user_id == local_user_id)
3640 3687 return query.first()
3641 3688
3642 3689 @classmethod
3643 3690 def user_by_external_id_and_provider(cls, external_id, provider_name):
3644 3691 """
3645 3692 Returns User instance based on search params
3646 3693
3647 3694 :param external_id:
3648 3695 :param provider_name:
3649 3696 :return: User
3650 3697 """
3651 3698 query = User.query()
3652 3699 query = query.filter(cls.external_id == external_id)
3653 3700 query = query.filter(cls.provider_name == provider_name)
3654 3701 query = query.filter(User.user_id == cls.local_user_id)
3655 3702 return query.first()
3656 3703
3657 3704 @classmethod
3658 3705 def by_local_user_id(cls, local_user_id):
3659 3706 """
3660 3707 Returns all tokens for user
3661 3708
3662 3709 :param local_user_id:
3663 3710 :return: ExternalIdentity
3664 3711 """
3665 3712 query = cls.query()
3666 3713 query = query.filter(cls.local_user_id == local_user_id)
3667 3714 return query
3668 3715
3669 3716
3670 3717 class Integration(Base, BaseModel):
3671 3718 __tablename__ = 'integrations'
3672 3719 __table_args__ = (
3673 3720 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3674 3721 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3675 3722 )
3676 3723
3677 3724 integration_id = Column('integration_id', Integer(), primary_key=True)
3678 3725 integration_type = Column('integration_type', String(255))
3679 3726 enabled = Column('enabled', Boolean(), nullable=False)
3680 3727 name = Column('name', String(255), nullable=False)
3681 3728 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3682 3729 default=False)
3683 3730
3684 3731 settings = Column(
3685 3732 'settings_json', MutationObj.as_mutable(
3686 3733 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3687 3734 repo_id = Column(
3688 3735 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3689 3736 nullable=True, unique=None, default=None)
3690 3737 repo = relationship('Repository', lazy='joined')
3691 3738
3692 3739 repo_group_id = Column(
3693 3740 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3694 3741 nullable=True, unique=None, default=None)
3695 3742 repo_group = relationship('RepoGroup', lazy='joined')
3696 3743
3697 3744 @property
3698 3745 def scope(self):
3699 3746 if self.repo:
3700 3747 return repr(self.repo)
3701 3748 if self.repo_group:
3702 3749 if self.child_repos_only:
3703 3750 return repr(self.repo_group) + ' (child repos only)'
3704 3751 else:
3705 3752 return repr(self.repo_group) + ' (recursive)'
3706 3753 if self.child_repos_only:
3707 3754 return 'root_repos'
3708 3755 return 'global'
3709 3756
3710 3757 def __repr__(self):
3711 3758 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3712 3759
3713 3760
3714 3761 class RepoReviewRuleUser(Base, BaseModel):
3715 3762 __tablename__ = 'repo_review_rules_users'
3716 3763 __table_args__ = (
3717 3764 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3718 3765 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3719 3766 )
3720 3767 repo_review_rule_user_id = Column(
3721 3768 'repo_review_rule_user_id', Integer(), primary_key=True)
3722 3769 repo_review_rule_id = Column("repo_review_rule_id",
3723 3770 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3724 3771 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3725 3772 nullable=False)
3726 3773 user = relationship('User')
3727 3774
3728 3775
3729 3776 class RepoReviewRuleUserGroup(Base, BaseModel):
3730 3777 __tablename__ = 'repo_review_rules_users_groups'
3731 3778 __table_args__ = (
3732 3779 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3733 3780 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3734 3781 )
3735 3782 repo_review_rule_users_group_id = Column(
3736 3783 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3737 3784 repo_review_rule_id = Column("repo_review_rule_id",
3738 3785 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3739 3786 users_group_id = Column("users_group_id", Integer(),
3740 3787 ForeignKey('users_groups.users_group_id'), nullable=False)
3741 3788 users_group = relationship('UserGroup')
3742 3789
3743 3790
3744 3791 class RepoReviewRule(Base, BaseModel):
3745 3792 __tablename__ = 'repo_review_rules'
3746 3793 __table_args__ = (
3747 3794 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3748 3795 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3749 3796 )
3750 3797
3751 3798 repo_review_rule_id = Column(
3752 3799 'repo_review_rule_id', Integer(), primary_key=True)
3753 3800 repo_id = Column(
3754 3801 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3755 3802 repo = relationship('Repository', backref='review_rules')
3756 3803
3757 3804 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3758 3805 default=u'*') # glob
3759 3806 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3760 3807 default=u'*') # glob
3761 3808
3762 3809 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3763 3810 nullable=False, default=False)
3764 3811 rule_users = relationship('RepoReviewRuleUser')
3765 3812 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3766 3813
3767 3814 @hybrid_property
3768 3815 def branch_pattern(self):
3769 3816 return self._branch_pattern or '*'
3770 3817
3771 3818 def _validate_glob(self, value):
3772 3819 re.compile('^' + glob2re(value) + '$')
3773 3820
3774 3821 @branch_pattern.setter
3775 3822 def branch_pattern(self, value):
3776 3823 self._validate_glob(value)
3777 3824 self._branch_pattern = value or '*'
3778 3825
3779 3826 @hybrid_property
3780 3827 def file_pattern(self):
3781 3828 return self._file_pattern or '*'
3782 3829
3783 3830 @file_pattern.setter
3784 3831 def file_pattern(self, value):
3785 3832 self._validate_glob(value)
3786 3833 self._file_pattern = value or '*'
3787 3834
3788 3835 def matches(self, branch, files_changed):
3789 3836 """
3790 3837 Check if this review rule matches a branch/files in a pull request
3791 3838
3792 3839 :param branch: branch name for the commit
3793 3840 :param files_changed: list of file paths changed in the pull request
3794 3841 """
3795 3842
3796 3843 branch = branch or ''
3797 3844 files_changed = files_changed or []
3798 3845
3799 3846 branch_matches = True
3800 3847 if branch:
3801 3848 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3802 3849 branch_matches = bool(branch_regex.search(branch))
3803 3850
3804 3851 files_matches = True
3805 3852 if self.file_pattern != '*':
3806 3853 files_matches = False
3807 3854 file_regex = re.compile(glob2re(self.file_pattern))
3808 3855 for filename in files_changed:
3809 3856 if file_regex.search(filename):
3810 3857 files_matches = True
3811 3858 break
3812 3859
3813 3860 return branch_matches and files_matches
3814 3861
3815 3862 @property
3816 3863 def review_users(self):
3817 3864 """ Returns the users which this rule applies to """
3818 3865
3819 3866 users = set()
3820 3867 users |= set([
3821 3868 rule_user.user for rule_user in self.rule_users
3822 3869 if rule_user.user.active])
3823 3870 users |= set(
3824 3871 member.user
3825 3872 for rule_user_group in self.rule_user_groups
3826 3873 for member in rule_user_group.users_group.members
3827 3874 if member.user.active
3828 3875 )
3829 3876 return users
3830 3877
3831 3878 def __repr__(self):
3832 3879 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3833 3880 self.repo_review_rule_id, self.repo)
3834 3881
3835 3882
3836 3883 class DbMigrateVersion(Base, BaseModel):
3837 3884 __tablename__ = 'db_migrate_version'
3838 3885 __table_args__ = (
3839 3886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3840 3887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3841 3888 )
3842 3889 repository_id = Column('repository_id', String(250), primary_key=True)
3843 3890 repository_path = Column('repository_path', Text)
3844 3891 version = Column('version', Integer)
3845 3892
3846 3893
3847 3894 class DbSession(Base, BaseModel):
3848 3895 __tablename__ = 'db_session'
3849 3896 __table_args__ = (
3850 3897 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3851 3898 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3852 3899 )
3853 3900
3854 3901 def __repr__(self):
3855 3902 return '<DB:DbSession({})>'.format(self.id)
3856 3903
3857 3904 id = Column('id', Integer())
3858 3905 namespace = Column('namespace', String(255), primary_key=True)
3859 3906 accessed = Column('accessed', DateTime, nullable=False)
3860 3907 created = Column('created', DateTime, nullable=False)
3861 3908 data = Column('data', PickleType, nullable=False)
@@ -1,54 +1,75 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 from rhodecode.model.auth_token import AuthTokenModel
21 21 from rhodecode.model.db import User
22 22 from rhodecode.tests import *
23 23
24 24
25 25 class TestFeedController(TestController):
26 26
27 27 def test_rss(self, backend):
28 28 self.log_user()
29 29 response = self.app.get(url(controller='feed', action='rss',
30 30 repo_name=backend.repo_name))
31 31
32 32 assert response.content_type == "application/rss+xml"
33 33 assert """<rss version="2.0">""" in response
34 34
35 def test_rss_with_auth_token(self, backend):
36 auth_token = User.get_first_super_admin().feed_token
35 def test_rss_with_auth_token(self, backend, user_admin):
36 auth_token = user_admin.feed_token
37 37 assert auth_token != ''
38 response = self.app.get(url(controller='feed', action='rss',
39 repo_name=backend.repo_name, auth_token=auth_token))
38 response = self.app.get(
39 url(controller='feed', action='rss',
40 repo_name=backend.repo_name, auth_token=auth_token,
41 status=200))
40 42
41 43 assert response.content_type == "application/rss+xml"
42 44 assert """<rss version="2.0">""" in response
43 45
46 def test_rss_with_auth_token_of_wrong_type(self, backend, user_util):
47 user = user_util.create_user()
48 auth_token = AuthTokenModel().create(
49 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
50 auth_token = auth_token.api_key
51
52 self.app.get(
53 url(controller='feed', action='rss',
54 repo_name=backend.repo_name, auth_token=auth_token),
55 status=302)
56
57 auth_token = AuthTokenModel().create(
58 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
59 auth_token = auth_token.api_key
60 self.app.get(
61 url(controller='feed', action='rss',
62 repo_name=backend.repo_name, auth_token=auth_token),
63 status=200)
64
44 65 def test_atom(self, backend):
45 66 self.log_user()
46 67 response = self.app.get(url(controller='feed', action='atom',
47 68 repo_name=backend.repo_name))
48 69
49 70 assert response.content_type == """application/atom+xml"""
50 71 assert """<?xml version="1.0" encoding="utf-8"?>""" in response
51 72
52 73 tag1 = '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'
53 74 tag2 = '<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom">'
54 75 assert tag1 in response or tag2 in response
@@ -1,588 +1,591 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 27 from rhodecode.tests import (
28 28 TestController, assert_session_flash, clear_all_caches, url,
29 29 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.tests.utils import AssertResponse, get_session_from_response
32 32 from rhodecode.lib.auth import check_password, generate_auth_token
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model.auth_token import AuthTokenModel
35 35 from rhodecode.model import validators
36 36 from rhodecode.model.db import User, Notification
37 37 from rhodecode.model.meta import Session
38 38
39 39 fixture = Fixture()
40 40
41 41 # Hardcode URLs because we don't have a request object to use
42 42 # pyramids URL generation methods.
43 43 index_url = '/'
44 44 login_url = ADMIN_PREFIX + '/login'
45 45 logut_url = ADMIN_PREFIX + '/logout'
46 46 register_url = ADMIN_PREFIX + '/register'
47 47 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
48 48 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
49 49
50 50
51 51 @pytest.mark.usefixtures('app')
52 52 class TestLoginController:
53 53 destroy_users = set()
54 54
55 55 @classmethod
56 56 def teardown_class(cls):
57 57 fixture.destroy_users(cls.destroy_users)
58 58
59 59 def teardown_method(self, method):
60 60 for n in Notification.query().all():
61 61 Session().delete(n)
62 62
63 63 Session().commit()
64 64 assert Notification.query().all() == []
65 65
66 66 def test_index(self):
67 67 response = self.app.get(login_url)
68 68 assert response.status == '200 OK'
69 69 # Test response...
70 70
71 71 def test_login_admin_ok(self):
72 72 response = self.app.post(login_url,
73 73 {'username': 'test_admin',
74 74 'password': 'test12'})
75 75 assert response.status == '302 Found'
76 76 session = get_session_from_response(response)
77 77 username = session['rhodecode_user'].get('username')
78 78 assert username == 'test_admin'
79 79 response = response.follow()
80 80 response.mustcontain('/%s' % HG_REPO)
81 81
82 82 def test_login_regular_ok(self):
83 83 response = self.app.post(login_url,
84 84 {'username': 'test_regular',
85 85 'password': 'test12'})
86 86
87 87 assert response.status == '302 Found'
88 88 session = get_session_from_response(response)
89 89 username = session['rhodecode_user'].get('username')
90 90 assert username == 'test_regular'
91 91 response = response.follow()
92 92 response.mustcontain('/%s' % HG_REPO)
93 93
94 94 def test_login_ok_came_from(self):
95 95 test_came_from = '/_admin/users?branch=stable'
96 96 _url = '{}?came_from={}'.format(login_url, test_came_from)
97 97 response = self.app.post(
98 98 _url, {'username': 'test_admin', 'password': 'test12'})
99 99 assert response.status == '302 Found'
100 100 assert 'branch=stable' in response.location
101 101 response = response.follow()
102 102
103 103 assert response.status == '200 OK'
104 104 response.mustcontain('Users administration')
105 105
106 106 def test_redirect_to_login_with_get_args(self):
107 107 with fixture.anon_access(False):
108 108 kwargs = {'branch': 'stable'}
109 109 response = self.app.get(
110 110 url('summary_home', repo_name=HG_REPO, **kwargs))
111 111 assert response.status == '302 Found'
112 112 response_query = urlparse.parse_qsl(response.location)
113 113 assert 'branch=stable' in response_query[0][1]
114 114
115 115 def test_login_form_with_get_args(self):
116 116 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
117 117 response = self.app.get(_url)
118 118 assert 'branch%3Dstable' in response.form.action
119 119
120 120 @pytest.mark.parametrize("url_came_from", [
121 121 'data:text/html,<script>window.alert("xss")</script>',
122 122 'mailto:test@rhodecode.org',
123 123 'file:///etc/passwd',
124 124 'ftp://some.ftp.server',
125 125 'http://other.domain',
126 126 '/\r\nX-Forwarded-Host: http://example.org',
127 127 ])
128 128 def test_login_bad_came_froms(self, url_came_from):
129 129 _url = '{}?came_from={}'.format(login_url, url_came_from)
130 130 response = self.app.post(
131 131 _url,
132 132 {'username': 'test_admin', 'password': 'test12'})
133 133 assert response.status == '302 Found'
134 134 response = response.follow()
135 135 assert response.status == '200 OK'
136 136 assert response.request.path == '/'
137 137
138 138 def test_login_short_password(self):
139 139 response = self.app.post(login_url,
140 140 {'username': 'test_admin',
141 141 'password': 'as'})
142 142 assert response.status == '200 OK'
143 143
144 144 response.mustcontain('Enter 3 characters or more')
145 145
146 146 def test_login_wrong_non_ascii_password(self, user_regular):
147 147 response = self.app.post(
148 148 login_url,
149 149 {'username': user_regular.username,
150 150 'password': u'invalid-non-asci\xe4'.encode('utf8')})
151 151
152 152 response.mustcontain('invalid user name')
153 153 response.mustcontain('invalid password')
154 154
155 155 def test_login_with_non_ascii_password(self, user_util):
156 156 password = u'valid-non-ascii\xe4'
157 157 user = user_util.create_user(password=password)
158 158 response = self.app.post(
159 159 login_url,
160 160 {'username': user.username,
161 161 'password': password.encode('utf-8')})
162 162 assert response.status_code == 302
163 163
164 164 def test_login_wrong_username_password(self):
165 165 response = self.app.post(login_url,
166 166 {'username': 'error',
167 167 'password': 'test12'})
168 168
169 169 response.mustcontain('invalid user name')
170 170 response.mustcontain('invalid password')
171 171
172 172 def test_login_admin_ok_password_migration(self, real_crypto_backend):
173 173 from rhodecode.lib import auth
174 174
175 175 # create new user, with sha256 password
176 176 temp_user = 'test_admin_sha256'
177 177 user = fixture.create_user(temp_user)
178 178 user.password = auth._RhodeCodeCryptoSha256().hash_create(
179 179 b'test123')
180 180 Session().add(user)
181 181 Session().commit()
182 182 self.destroy_users.add(temp_user)
183 183 response = self.app.post(login_url,
184 184 {'username': temp_user,
185 185 'password': 'test123'})
186 186
187 187 assert response.status == '302 Found'
188 188 session = get_session_from_response(response)
189 189 username = session['rhodecode_user'].get('username')
190 190 assert username == temp_user
191 191 response = response.follow()
192 192 response.mustcontain('/%s' % HG_REPO)
193 193
194 194 # new password should be bcrypted, after log-in and transfer
195 195 user = User.get_by_username(temp_user)
196 196 assert user.password.startswith('$')
197 197
198 198 # REGISTRATIONS
199 199 def test_register(self):
200 200 response = self.app.get(register_url)
201 201 response.mustcontain('Create an Account')
202 202
203 203 def test_register_err_same_username(self):
204 204 uname = 'test_admin'
205 205 response = self.app.post(
206 206 register_url,
207 207 {
208 208 'username': uname,
209 209 'password': 'test12',
210 210 'password_confirmation': 'test12',
211 211 'email': 'goodmail@domain.com',
212 212 'firstname': 'test',
213 213 'lastname': 'test'
214 214 }
215 215 )
216 216
217 217 assertr = AssertResponse(response)
218 218 msg = validators.ValidUsername()._messages['username_exists']
219 219 msg = msg % {'username': uname}
220 220 assertr.element_contains('#username+.error-message', msg)
221 221
222 222 def test_register_err_same_email(self):
223 223 response = self.app.post(
224 224 register_url,
225 225 {
226 226 'username': 'test_admin_0',
227 227 'password': 'test12',
228 228 'password_confirmation': 'test12',
229 229 'email': 'test_admin@mail.com',
230 230 'firstname': 'test',
231 231 'lastname': 'test'
232 232 }
233 233 )
234 234
235 235 assertr = AssertResponse(response)
236 236 msg = validators.UniqSystemEmail()()._messages['email_taken']
237 237 assertr.element_contains('#email+.error-message', msg)
238 238
239 239 def test_register_err_same_email_case_sensitive(self):
240 240 response = self.app.post(
241 241 register_url,
242 242 {
243 243 'username': 'test_admin_1',
244 244 'password': 'test12',
245 245 'password_confirmation': 'test12',
246 246 'email': 'TesT_Admin@mail.COM',
247 247 'firstname': 'test',
248 248 'lastname': 'test'
249 249 }
250 250 )
251 251 assertr = AssertResponse(response)
252 252 msg = validators.UniqSystemEmail()()._messages['email_taken']
253 253 assertr.element_contains('#email+.error-message', msg)
254 254
255 255 def test_register_err_wrong_data(self):
256 256 response = self.app.post(
257 257 register_url,
258 258 {
259 259 'username': 'xs',
260 260 'password': 'test',
261 261 'password_confirmation': 'test',
262 262 'email': 'goodmailm',
263 263 'firstname': 'test',
264 264 'lastname': 'test'
265 265 }
266 266 )
267 267 assert response.status == '200 OK'
268 268 response.mustcontain('An email address must contain a single @')
269 269 response.mustcontain('Enter a value 6 characters long or more')
270 270
271 271 def test_register_err_username(self):
272 272 response = self.app.post(
273 273 register_url,
274 274 {
275 275 'username': 'error user',
276 276 'password': 'test12',
277 277 'password_confirmation': 'test12',
278 278 'email': 'goodmailm',
279 279 'firstname': 'test',
280 280 'lastname': 'test'
281 281 }
282 282 )
283 283
284 284 response.mustcontain('An email address must contain a single @')
285 285 response.mustcontain(
286 286 'Username may only contain '
287 287 'alphanumeric characters underscores, '
288 288 'periods or dashes and must begin with '
289 289 'alphanumeric character')
290 290
291 291 def test_register_err_case_sensitive(self):
292 292 usr = 'Test_Admin'
293 293 response = self.app.post(
294 294 register_url,
295 295 {
296 296 'username': usr,
297 297 'password': 'test12',
298 298 'password_confirmation': 'test12',
299 299 'email': 'goodmailm',
300 300 'firstname': 'test',
301 301 'lastname': 'test'
302 302 }
303 303 )
304 304
305 305 assertr = AssertResponse(response)
306 306 msg = validators.ValidUsername()._messages['username_exists']
307 307 msg = msg % {'username': usr}
308 308 assertr.element_contains('#username+.error-message', msg)
309 309
310 310 def test_register_special_chars(self):
311 311 response = self.app.post(
312 312 register_url,
313 313 {
314 314 'username': 'xxxaxn',
315 315 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
316 316 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
317 317 'email': 'goodmailm@test.plx',
318 318 'firstname': 'test',
319 319 'lastname': 'test'
320 320 }
321 321 )
322 322
323 323 msg = validators.ValidPassword()._messages['invalid_password']
324 324 response.mustcontain(msg)
325 325
326 326 def test_register_password_mismatch(self):
327 327 response = self.app.post(
328 328 register_url,
329 329 {
330 330 'username': 'xs',
331 331 'password': '123qwe',
332 332 'password_confirmation': 'qwe123',
333 333 'email': 'goodmailm@test.plxa',
334 334 'firstname': 'test',
335 335 'lastname': 'test'
336 336 }
337 337 )
338 338 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
339 339 response.mustcontain(msg)
340 340
341 341 def test_register_ok(self):
342 342 username = 'test_regular4'
343 343 password = 'qweqwe'
344 344 email = 'marcin@test.com'
345 345 name = 'testname'
346 346 lastname = 'testlastname'
347 347
348 348 response = self.app.post(
349 349 register_url,
350 350 {
351 351 'username': username,
352 352 'password': password,
353 353 'password_confirmation': password,
354 354 'email': email,
355 355 'firstname': name,
356 356 'lastname': lastname,
357 357 'admin': True
358 358 }
359 359 ) # This should be overriden
360 360 assert response.status == '302 Found'
361 361 assert_session_flash(
362 362 response, 'You have successfully registered with RhodeCode')
363 363
364 364 ret = Session().query(User).filter(
365 365 User.username == 'test_regular4').one()
366 366 assert ret.username == username
367 367 assert check_password(password, ret.password)
368 368 assert ret.email == email
369 369 assert ret.name == name
370 370 assert ret.lastname == lastname
371 371 assert ret.api_key is not None
372 372 assert not ret.admin
373 373
374 374 def test_forgot_password_wrong_mail(self):
375 375 bad_email = 'marcin@wrongmail.org'
376 376 response = self.app.post(
377 377 pwd_reset_url,
378 378 {'email': bad_email, }
379 379 )
380 380
381 381 msg = validators.ValidSystemEmail()._messages['non_existing_email']
382 382 msg = h.html_escape(msg % {'email': bad_email})
383 383 response.mustcontain()
384 384
385 385 def test_forgot_password(self):
386 386 response = self.app.get(pwd_reset_url)
387 387 assert response.status == '200 OK'
388 388
389 389 username = 'test_password_reset_1'
390 390 password = 'qweqwe'
391 391 email = 'marcin@python-works.com'
392 392 name = 'passwd'
393 393 lastname = 'reset'
394 394
395 395 new = User()
396 396 new.username = username
397 397 new.password = password
398 398 new.email = email
399 399 new.name = name
400 400 new.lastname = lastname
401 401 new.api_key = generate_auth_token(username)
402 402 Session().add(new)
403 403 Session().commit()
404 404
405 405 response = self.app.post(pwd_reset_url,
406 406 {'email': email, })
407 407
408 408 assert_session_flash(
409 409 response, 'Your password reset link was sent')
410 410
411 411 response = response.follow()
412 412
413 413 # BAD KEY
414 414
415 415 key = "bad"
416 416 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
417 417 response = self.app.get(confirm_url)
418 418 assert response.status == '302 Found'
419 419 assert response.location.endswith(pwd_reset_url)
420 420
421 421 # GOOD KEY
422 422
423 423 key = User.get_by_username(username).api_key
424 424 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
425 425 response = self.app.get(confirm_url)
426 426 assert response.status == '302 Found'
427 427 assert response.location.endswith(login_url)
428 428
429 429 assert_session_flash(
430 430 response,
431 431 'Your password reset was successful, '
432 432 'a new password has been sent to your email')
433 433
434 434 response = response.follow()
435 435
436 436 def _get_api_whitelist(self, values=None):
437 437 config = {'api_access_controllers_whitelist': values or []}
438 438 return config
439 439
440 440 @pytest.mark.parametrize("test_name, auth_token", [
441 441 ('none', None),
442 442 ('empty_string', ''),
443 443 ('fake_number', '123456'),
444 444 ('proper_auth_token', None)
445 445 ])
446 def test_access_not_whitelisted_page_via_auth_token(self, test_name,
447 auth_token):
446 def test_access_not_whitelisted_page_via_auth_token(
447 self, test_name, auth_token, user_admin):
448
448 449 whitelist = self._get_api_whitelist([])
449 450 with mock.patch.dict('rhodecode.CONFIG', whitelist):
450 451 assert [] == whitelist['api_access_controllers_whitelist']
451 452 if test_name == 'proper_auth_token':
452 453 # use builtin if api_key is None
453 auth_token = User.get_first_super_admin().api_key
454 auth_token = user_admin.api_key
454 455
455 456 with fixture.anon_access(False):
456 457 self.app.get(url(controller='changeset',
457 458 action='changeset_raw',
458 459 repo_name=HG_REPO, revision='tip',
459 460 api_key=auth_token),
460 461 status=302)
461 462
462 463 @pytest.mark.parametrize("test_name, auth_token, code", [
463 464 ('none', None, 302),
464 465 ('empty_string', '', 302),
465 466 ('fake_number', '123456', 302),
466 467 ('proper_auth_token', None, 200)
467 468 ])
468 def test_access_whitelisted_page_via_auth_token(self, test_name,
469 auth_token, code):
470 whitelist = self._get_api_whitelist(
471 ['ChangesetController:changeset_raw'])
469 def test_access_whitelisted_page_via_auth_token(
470 self, test_name, auth_token, code, user_admin):
471
472 whitelist_entry = ['ChangesetController:changeset_raw']
473 whitelist = self._get_api_whitelist(whitelist_entry)
474
472 475 with mock.patch.dict('rhodecode.CONFIG', whitelist):
473 assert ['ChangesetController:changeset_raw'] == \
474 whitelist['api_access_controllers_whitelist']
476 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
477
475 478 if test_name == 'proper_auth_token':
476 auth_token = User.get_first_super_admin().api_key
479 auth_token = user_admin.api_key
477 480
478 481 with fixture.anon_access(False):
479 482 self.app.get(url(controller='changeset',
480 483 action='changeset_raw',
481 484 repo_name=HG_REPO, revision='tip',
482 485 api_key=auth_token),
483 486 status=code)
484 487
485 488 def test_access_page_via_extra_auth_token(self):
486 489 whitelist = self._get_api_whitelist(
487 490 ['ChangesetController:changeset_raw'])
488 491 with mock.patch.dict('rhodecode.CONFIG', whitelist):
489 492 assert ['ChangesetController:changeset_raw'] == \
490 493 whitelist['api_access_controllers_whitelist']
491 494
492 495 new_auth_token = AuthTokenModel().create(
493 496 TEST_USER_ADMIN_LOGIN, 'test')
494 497 Session().commit()
495 498 with fixture.anon_access(False):
496 499 self.app.get(url(controller='changeset',
497 500 action='changeset_raw',
498 501 repo_name=HG_REPO, revision='tip',
499 502 api_key=new_auth_token.api_key),
500 503 status=200)
501 504
502 505 def test_access_page_via_expired_auth_token(self):
503 506 whitelist = self._get_api_whitelist(
504 507 ['ChangesetController:changeset_raw'])
505 508 with mock.patch.dict('rhodecode.CONFIG', whitelist):
506 509 assert ['ChangesetController:changeset_raw'] == \
507 510 whitelist['api_access_controllers_whitelist']
508 511
509 512 new_auth_token = AuthTokenModel().create(
510 513 TEST_USER_ADMIN_LOGIN, 'test')
511 514 Session().commit()
512 515 # patch the api key and make it expired
513 516 new_auth_token.expires = 0
514 517 Session().add(new_auth_token)
515 518 Session().commit()
516 519 with fixture.anon_access(False):
517 520 self.app.get(url(controller='changeset',
518 521 action='changeset_raw',
519 522 repo_name=HG_REPO, revision='tip',
520 523 api_key=new_auth_token.api_key),
521 524 status=302)
522 525
523 526
524 527 class TestPasswordReset(TestController):
525 528
526 529 @pytest.mark.parametrize(
527 530 'pwd_reset_setting, show_link, show_reset', [
528 531 ('hg.password_reset.enabled', True, True),
529 532 ('hg.password_reset.hidden', False, True),
530 533 ('hg.password_reset.disabled', False, False),
531 534 ])
532 535 def test_password_reset_settings(
533 536 self, pwd_reset_setting, show_link, show_reset):
534 537 clear_all_caches()
535 538 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
536 539 params = {
537 540 'csrf_token': self.csrf_token,
538 541 'anonymous': 'True',
539 542 'default_register': 'hg.register.auto_activate',
540 543 'default_register_message': '',
541 544 'default_password_reset': pwd_reset_setting,
542 545 'default_extern_activate': 'hg.extern_activate.auto',
543 546 }
544 547 resp = self.app.post(url('admin_permissions_application'), params=params)
545 548 self.logout_user()
546 549
547 550 login_page = self.app.get(login_url)
548 551 asr_login = AssertResponse(login_page)
549 552 index_page = self.app.get(index_url)
550 553 asr_index = AssertResponse(index_page)
551 554
552 555 if show_link:
553 556 asr_login.one_element_exists('a.pwd_reset')
554 557 asr_index.one_element_exists('a.pwd_reset')
555 558 else:
556 559 asr_login.no_element_exists('a.pwd_reset')
557 560 asr_index.no_element_exists('a.pwd_reset')
558 561
559 562 pwdreset_page = self.app.get(pwd_reset_url)
560 563
561 564 asr_reset = AssertResponse(pwdreset_page)
562 565 if show_reset:
563 566 assert 'Send password reset email' in pwdreset_page
564 567 asr_reset.one_element_exists('#email')
565 568 asr_reset.one_element_exists('#send')
566 569 else:
567 570 assert 'Password reset is disabled.' in pwdreset_page
568 571 asr_reset.no_element_exists('#email')
569 572 asr_reset.no_element_exists('#send')
570 573
571 574 def test_password_form_disabled(self):
572 575 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
573 576 params = {
574 577 'csrf_token': self.csrf_token,
575 578 'anonymous': 'True',
576 579 'default_register': 'hg.register.auto_activate',
577 580 'default_register_message': '',
578 581 'default_password_reset': 'hg.password_reset.disabled',
579 582 'default_extern_activate': 'hg.extern_activate.auto',
580 583 }
581 584 self.app.post(url('admin_permissions_application'), params=params)
582 585 self.logout_user()
583 586
584 587 pwdreset_page = self.app.post(
585 588 pwd_reset_url,
586 589 {'email': 'lisa@rhodecode.com',}
587 590 )
588 591 assert 'Password reset is disabled.' in pwdreset_page
@@ -1,582 +1,608 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 from hashlib import sha1
23 23
24 24 import pytest
25 25 from mock import patch
26 26
27 27 from rhodecode.lib import auth
28 28 from rhodecode.lib.utils2 import md5
29 from rhodecode.model.auth_token import AuthTokenModel
29 30 from rhodecode.model.db import User
30 31 from rhodecode.model.repo import RepoModel
31 32 from rhodecode.model.user import UserModel
32 33 from rhodecode.model.user_group import UserGroupModel
33 34
34 35
35 36 def test_perm_origin_dict():
36 37 pod = auth.PermOriginDict()
37 38 pod['thing'] = 'read', 'default'
38 39 assert pod['thing'] == 'read'
39 40
40 41 assert pod.perm_origin_stack == {
41 42 'thing': [('read', 'default')]}
42 43
43 44 pod['thing'] = 'write', 'admin'
44 45 assert pod['thing'] == 'write'
45 46
46 47 assert pod.perm_origin_stack == {
47 48 'thing': [('read', 'default'), ('write', 'admin')]}
48 49
49 50 pod['other'] = 'write', 'default'
50 51
51 52 assert pod.perm_origin_stack == {
52 53 'other': [('write', 'default')],
53 54 'thing': [('read', 'default'), ('write', 'admin')]}
54 55
55 56 pod['other'] = 'none', 'override'
56 57
57 58 assert pod.perm_origin_stack == {
58 59 'other': [('write', 'default'), ('none', 'override')],
59 60 'thing': [('read', 'default'), ('write', 'admin')]}
60 61
61 62 with pytest.raises(ValueError):
62 63 pod['thing'] = 'read'
63 64
64 65
65 66 def test_cached_perms_data(user_regular, backend_random):
66 67 permissions = get_permissions(user_regular)
67 68 repo_name = backend_random.repo.repo_name
68 69 expected_global_permissions = {
69 70 'repository.read', 'group.read', 'usergroup.read'}
70 71 assert expected_global_permissions.issubset(permissions['global'])
71 72 assert permissions['repositories'][repo_name] == 'repository.read'
72 73
73 74
74 75 def test_cached_perms_data_with_admin_user(user_regular, backend_random):
75 76 permissions = get_permissions(user_regular, user_is_admin=True)
76 77 repo_name = backend_random.repo.repo_name
77 78 assert 'hg.admin' in permissions['global']
78 79 assert permissions['repositories'][repo_name] == 'repository.admin'
79 80
80 81
81 82 def test_cached_perms_data_user_group_global_permissions(user_util):
82 83 user, user_group = user_util.create_user_with_group()
83 84 user_group.inherit_default_permissions = False
84 85
85 86 granted_permission = 'repository.write'
86 87 UserGroupModel().grant_perm(user_group, granted_permission)
87 88
88 89 permissions = get_permissions(user)
89 90 assert granted_permission in permissions['global']
90 91
91 92
92 93 @pytest.mark.xfail(reason="Not implemented, see TODO note")
93 94 def test_cached_perms_data_user_group_global_permissions_(user_util):
94 95 user, user_group = user_util.create_user_with_group()
95 96
96 97 granted_permission = 'repository.write'
97 98 UserGroupModel().grant_perm(user_group, granted_permission)
98 99
99 100 permissions = get_permissions(user)
100 101 assert granted_permission in permissions['global']
101 102
102 103
103 104 def test_cached_perms_data_user_global_permissions(user_util):
104 105 user = user_util.create_user()
105 106 UserModel().grant_perm(user, 'repository.none')
106 107
107 108 permissions = get_permissions(user, user_inherit_default_permissions=True)
108 109 assert 'repository.read' in permissions['global']
109 110
110 111
111 112 def test_cached_perms_data_repository_permissions_on_private_repository(
112 113 backend_random, user_util):
113 114 user, user_group = user_util.create_user_with_group()
114 115
115 116 repo = backend_random.create_repo()
116 117 repo.private = True
117 118
118 119 granted_permission = 'repository.write'
119 120 RepoModel().grant_user_group_permission(
120 121 repo, user_group.users_group_name, granted_permission)
121 122
122 123 permissions = get_permissions(user)
123 124 assert permissions['repositories'][repo.repo_name] == granted_permission
124 125
125 126
126 127 def test_cached_perms_data_repository_permissions_for_owner(
127 128 backend_random, user_util):
128 129 user = user_util.create_user()
129 130
130 131 repo = backend_random.create_repo()
131 132 repo.user_id = user.user_id
132 133
133 134 permissions = get_permissions(user)
134 135 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
135 136
136 137 # TODO: johbo: Make cleanup in UserUtility smarter, then remove this hack
137 138 repo.user_id = User.get_default_user().user_id
138 139
139 140
140 141 def test_cached_perms_data_repository_permissions_not_inheriting_defaults(
141 142 backend_random, user_util):
142 143 user = user_util.create_user()
143 144 repo = backend_random.create_repo()
144 145
145 146 # Don't inherit default object permissions
146 147 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
147 148
148 149 permissions = get_permissions(user)
149 150 assert permissions['repositories'][repo.repo_name] == 'repository.none'
150 151
151 152
152 153 def test_cached_perms_data_default_permissions_on_repository_group(user_util):
153 154 # Have a repository group with default permissions set
154 155 repo_group = user_util.create_repo_group()
155 156 default_user = User.get_default_user()
156 157 user_util.grant_user_permission_to_repo_group(
157 158 repo_group, default_user, 'repository.write')
158 159 user = user_util.create_user()
159 160
160 161 permissions = get_permissions(user)
161 162 assert permissions['repositories_groups'][repo_group.group_name] == \
162 163 'repository.write'
163 164
164 165
165 166 def test_cached_perms_data_default_permissions_on_repository_group_owner(
166 167 user_util):
167 168 # Have a repository group
168 169 repo_group = user_util.create_repo_group()
169 170 default_user = User.get_default_user()
170 171
171 172 # Add a permission for the default user to hit the code path
172 173 user_util.grant_user_permission_to_repo_group(
173 174 repo_group, default_user, 'repository.write')
174 175
175 176 # Have an owner of the group
176 177 user = user_util.create_user()
177 178 repo_group.user_id = user.user_id
178 179
179 180 permissions = get_permissions(user)
180 181 assert permissions['repositories_groups'][repo_group.group_name] == \
181 182 'group.admin'
182 183
183 184
184 185 def test_cached_perms_data_default_permissions_on_repository_group_no_inherit(
185 186 user_util):
186 187 # Have a repository group
187 188 repo_group = user_util.create_repo_group()
188 189 default_user = User.get_default_user()
189 190
190 191 # Add a permission for the default user to hit the code path
191 192 user_util.grant_user_permission_to_repo_group(
192 193 repo_group, default_user, 'repository.write')
193 194
194 195 # Don't inherit default object permissions
195 196 user = user_util.create_user()
196 197 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
197 198
198 199 permissions = get_permissions(user)
199 200 assert permissions['repositories_groups'][repo_group.group_name] == \
200 201 'group.none'
201 202
202 203
203 204 def test_cached_perms_data_repository_permissions_from_user_group(
204 205 user_util, backend_random):
205 206 user, user_group = user_util.create_user_with_group()
206 207
207 208 # Needs a second user group to make sure that we select the right
208 209 # permissions.
209 210 user_group2 = user_util.create_user_group()
210 211 UserGroupModel().add_user_to_group(user_group2, user)
211 212
212 213 repo = backend_random.create_repo()
213 214
214 215 RepoModel().grant_user_group_permission(
215 216 repo, user_group.users_group_name, 'repository.read')
216 217 RepoModel().grant_user_group_permission(
217 218 repo, user_group2.users_group_name, 'repository.write')
218 219
219 220 permissions = get_permissions(user)
220 221 assert permissions['repositories'][repo.repo_name] == 'repository.write'
221 222
222 223
223 224 def test_cached_perms_data_repository_permissions_from_user_group_owner(
224 225 user_util, backend_random):
225 226 user, user_group = user_util.create_user_with_group()
226 227
227 228 repo = backend_random.create_repo()
228 229 repo.user_id = user.user_id
229 230
230 231 RepoModel().grant_user_group_permission(
231 232 repo, user_group.users_group_name, 'repository.write')
232 233
233 234 permissions = get_permissions(user)
234 235 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
235 236
236 237
237 238 def test_cached_perms_data_user_repository_permissions(
238 239 user_util, backend_random):
239 240 user = user_util.create_user()
240 241 repo = backend_random.create_repo()
241 242 granted_permission = 'repository.write'
242 243 RepoModel().grant_user_permission(repo, user, granted_permission)
243 244
244 245 permissions = get_permissions(user)
245 246 assert permissions['repositories'][repo.repo_name] == granted_permission
246 247
247 248
248 249 def test_cached_perms_data_user_repository_permissions_explicit(
249 250 user_util, backend_random):
250 251 user = user_util.create_user()
251 252 repo = backend_random.create_repo()
252 253 granted_permission = 'repository.none'
253 254 RepoModel().grant_user_permission(repo, user, granted_permission)
254 255
255 256 permissions = get_permissions(user, explicit=True)
256 257 assert permissions['repositories'][repo.repo_name] == granted_permission
257 258
258 259
259 260 def test_cached_perms_data_user_repository_permissions_owner(
260 261 user_util, backend_random):
261 262 user = user_util.create_user()
262 263 repo = backend_random.create_repo()
263 264 repo.user_id = user.user_id
264 265 RepoModel().grant_user_permission(repo, user, 'repository.write')
265 266
266 267 permissions = get_permissions(user)
267 268 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
268 269
269 270
270 271 def test_cached_perms_data_repository_groups_permissions_inherited(
271 272 user_util, backend_random):
272 273 user, user_group = user_util.create_user_with_group()
273 274
274 275 # Needs a second group to hit the last condition
275 276 user_group2 = user_util.create_user_group()
276 277 UserGroupModel().add_user_to_group(user_group2, user)
277 278
278 279 repo_group = user_util.create_repo_group()
279 280
280 281 user_util.grant_user_group_permission_to_repo_group(
281 282 repo_group, user_group, 'group.read')
282 283 user_util.grant_user_group_permission_to_repo_group(
283 284 repo_group, user_group2, 'group.write')
284 285
285 286 permissions = get_permissions(user)
286 287 assert permissions['repositories_groups'][repo_group.group_name] == \
287 288 'group.write'
288 289
289 290
290 291 def test_cached_perms_data_repository_groups_permissions_inherited_owner(
291 292 user_util, backend_random):
292 293 user, user_group = user_util.create_user_with_group()
293 294 repo_group = user_util.create_repo_group()
294 295 repo_group.user_id = user.user_id
295 296
296 297 granted_permission = 'group.write'
297 298 user_util.grant_user_group_permission_to_repo_group(
298 299 repo_group, user_group, granted_permission)
299 300
300 301 permissions = get_permissions(user)
301 302 assert permissions['repositories_groups'][repo_group.group_name] == \
302 303 'group.admin'
303 304
304 305
305 306 def test_cached_perms_data_repository_groups_permissions(
306 307 user_util, backend_random):
307 308 user = user_util.create_user()
308 309
309 310 repo_group = user_util.create_repo_group()
310 311
311 312 granted_permission = 'group.write'
312 313 user_util.grant_user_permission_to_repo_group(
313 314 repo_group, user, granted_permission)
314 315
315 316 permissions = get_permissions(user)
316 317 assert permissions['repositories_groups'][repo_group.group_name] == \
317 318 'group.write'
318 319
319 320
320 321 def test_cached_perms_data_repository_groups_permissions_explicit(
321 322 user_util, backend_random):
322 323 user = user_util.create_user()
323 324
324 325 repo_group = user_util.create_repo_group()
325 326
326 327 granted_permission = 'group.none'
327 328 user_util.grant_user_permission_to_repo_group(
328 329 repo_group, user, granted_permission)
329 330
330 331 permissions = get_permissions(user, explicit=True)
331 332 assert permissions['repositories_groups'][repo_group.group_name] == \
332 333 'group.none'
333 334
334 335
335 336 def test_cached_perms_data_repository_groups_permissions_owner(
336 337 user_util, backend_random):
337 338 user = user_util.create_user()
338 339
339 340 repo_group = user_util.create_repo_group()
340 341 repo_group.user_id = user.user_id
341 342
342 343 granted_permission = 'group.write'
343 344 user_util.grant_user_permission_to_repo_group(
344 345 repo_group, user, granted_permission)
345 346
346 347 permissions = get_permissions(user)
347 348 assert permissions['repositories_groups'][repo_group.group_name] == \
348 349 'group.admin'
349 350
350 351
351 352 def test_cached_perms_data_user_group_permissions_inherited(
352 353 user_util, backend_random):
353 354 user, user_group = user_util.create_user_with_group()
354 355 user_group2 = user_util.create_user_group()
355 356 UserGroupModel().add_user_to_group(user_group2, user)
356 357
357 358 target_user_group = user_util.create_user_group()
358 359
359 360 user_util.grant_user_group_permission_to_user_group(
360 361 target_user_group, user_group, 'usergroup.read')
361 362 user_util.grant_user_group_permission_to_user_group(
362 363 target_user_group, user_group2, 'usergroup.write')
363 364
364 365 permissions = get_permissions(user)
365 366 assert permissions['user_groups'][target_user_group.users_group_name] == \
366 367 'usergroup.write'
367 368
368 369
369 370 def test_cached_perms_data_user_group_permissions(
370 371 user_util, backend_random):
371 372 user = user_util.create_user()
372 373 user_group = user_util.create_user_group()
373 374 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.write')
374 375
375 376 permissions = get_permissions(user)
376 377 assert permissions['user_groups'][user_group.users_group_name] == \
377 378 'usergroup.write'
378 379
379 380
380 381 def test_cached_perms_data_user_group_permissions_explicit(
381 382 user_util, backend_random):
382 383 user = user_util.create_user()
383 384 user_group = user_util.create_user_group()
384 385 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.none')
385 386
386 387 permissions = get_permissions(user, explicit=True)
387 388 assert permissions['user_groups'][user_group.users_group_name] == \
388 389 'usergroup.none'
389 390
390 391
391 392 def test_cached_perms_data_user_group_permissions_not_inheriting_defaults(
392 393 user_util, backend_random):
393 394 user = user_util.create_user()
394 395 user_group = user_util.create_user_group()
395 396
396 397 # Don't inherit default object permissions
397 398 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
398 399
399 400 permissions = get_permissions(user)
400 401 assert permissions['user_groups'][user_group.users_group_name] == \
401 402 'usergroup.none'
402 403
403 404
404 405 def test_permission_calculator_admin_permissions(
405 406 user_util, backend_random):
406 407 user = user_util.create_user()
407 408 user_group = user_util.create_user_group()
408 409 repo = backend_random.repo
409 410 repo_group = user_util.create_repo_group()
410 411
411 412 calculator = auth.PermissionCalculator(
412 413 user.user_id, {}, False, False, True, 'higherwin')
413 414 permissions = calculator._admin_permissions()
414 415
415 416 assert permissions['repositories_groups'][repo_group.group_name] == \
416 417 'group.admin'
417 418 assert permissions['user_groups'][user_group.users_group_name] == \
418 419 'usergroup.admin'
419 420 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
420 421 assert 'hg.admin' in permissions['global']
421 422
422 423
423 424 def test_permission_calculator_repository_permissions_robustness_from_group(
424 425 user_util, backend_random):
425 426 user, user_group = user_util.create_user_with_group()
426 427
427 428 RepoModel().grant_user_group_permission(
428 429 backend_random.repo, user_group.users_group_name, 'repository.write')
429 430
430 431 calculator = auth.PermissionCalculator(
431 432 user.user_id, {}, False, False, False, 'higherwin')
432 433 calculator._calculate_repository_permissions()
433 434
434 435
435 436 def test_permission_calculator_repository_permissions_robustness_from_user(
436 437 user_util, backend_random):
437 438 user = user_util.create_user()
438 439
439 440 RepoModel().grant_user_permission(
440 441 backend_random.repo, user, 'repository.write')
441 442
442 443 calculator = auth.PermissionCalculator(
443 444 user.user_id, {}, False, False, False, 'higherwin')
444 445 calculator._calculate_repository_permissions()
445 446
446 447
447 448 def test_permission_calculator_repo_group_permissions_robustness_from_group(
448 449 user_util, backend_random):
449 450 user, user_group = user_util.create_user_with_group()
450 451 repo_group = user_util.create_repo_group()
451 452
452 453 user_util.grant_user_group_permission_to_repo_group(
453 454 repo_group, user_group, 'group.write')
454 455
455 456 calculator = auth.PermissionCalculator(
456 457 user.user_id, {}, False, False, False, 'higherwin')
457 458 calculator._calculate_repository_group_permissions()
458 459
459 460
460 461 def test_permission_calculator_repo_group_permissions_robustness_from_user(
461 462 user_util, backend_random):
462 463 user = user_util.create_user()
463 464 repo_group = user_util.create_repo_group()
464 465
465 466 user_util.grant_user_permission_to_repo_group(
466 467 repo_group, user, 'group.write')
467 468
468 469 calculator = auth.PermissionCalculator(
469 470 user.user_id, {}, False, False, False, 'higherwin')
470 471 calculator._calculate_repository_group_permissions()
471 472
472 473
473 474 def test_permission_calculator_user_group_permissions_robustness_from_group(
474 475 user_util, backend_random):
475 476 user, user_group = user_util.create_user_with_group()
476 477 target_user_group = user_util.create_user_group()
477 478
478 479 user_util.grant_user_group_permission_to_user_group(
479 480 target_user_group, user_group, 'usergroup.write')
480 481
481 482 calculator = auth.PermissionCalculator(
482 483 user.user_id, {}, False, False, False, 'higherwin')
483 484 calculator._calculate_user_group_permissions()
484 485
485 486
486 487 def test_permission_calculator_user_group_permissions_robustness_from_user(
487 488 user_util, backend_random):
488 489 user = user_util.create_user()
489 490 target_user_group = user_util.create_user_group()
490 491
491 492 user_util.grant_user_permission_to_user_group(
492 493 target_user_group, user, 'usergroup.write')
493 494
494 495 calculator = auth.PermissionCalculator(
495 496 user.user_id, {}, False, False, False, 'higherwin')
496 497 calculator._calculate_user_group_permissions()
497 498
498 499
499 500 @pytest.mark.parametrize("algo, new_permission, old_permission, expected", [
500 501 ('higherwin', 'repository.none', 'repository.none', 'repository.none'),
501 502 ('higherwin', 'repository.read', 'repository.none', 'repository.read'),
502 503 ('lowerwin', 'repository.write', 'repository.write', 'repository.write'),
503 504 ('lowerwin', 'repository.read', 'repository.write', 'repository.read'),
504 505 ])
505 506 def test_permission_calculator_choose_permission(
506 507 user_regular, algo, new_permission, old_permission, expected):
507 508 calculator = auth.PermissionCalculator(
508 509 user_regular.user_id, {}, False, False, False, algo)
509 510 result = calculator._choose_permission(new_permission, old_permission)
510 511 assert result == expected
511 512
512 513
513 514 def test_permission_calculator_choose_permission_raises_on_wrong_algo(
514 515 user_regular):
515 516 calculator = auth.PermissionCalculator(
516 517 user_regular.user_id, {}, False, False, False, 'invalid')
517 518 result = calculator._choose_permission(
518 519 'repository.read', 'repository.read')
519 520 # TODO: johbo: This documents the existing behavior. Think of an
520 521 # improvement.
521 522 assert result is None
522 523
523 524
524 525 def test_auth_user_get_cookie_store_for_normal_user(user_util):
525 526 user = user_util.create_user()
526 527 auth_user = auth.AuthUser(user_id=user.user_id)
527 528 expected_data = {
528 529 'username': user.username,
529 530 'user_id': user.user_id,
530 531 'password': md5(user.password),
531 532 'is_authenticated': False
532 533 }
533 534 assert auth_user.get_cookie_store() == expected_data
534 535
535 536
536 537 def test_auth_user_get_cookie_store_for_default_user():
537 538 default_user = User.get_default_user()
538 539 auth_user = auth.AuthUser()
539 540 expected_data = {
540 541 'username': User.DEFAULT_USER,
541 542 'user_id': default_user.user_id,
542 543 'password': md5(default_user.password),
543 544 'is_authenticated': True
544 545 }
545 546 assert auth_user.get_cookie_store() == expected_data
546 547
547 548
548 549 def get_permissions(user, **kwargs):
549 550 """
550 551 Utility filling in useful defaults into the call to `_cached_perms_data`.
551 552
552 553 Fill in `**kwargs` if specific values are needed for a test.
553 554 """
554 555 call_args = {
555 556 'user_id': user.user_id,
556 557 'scope': {},
557 558 'user_is_admin': False,
558 559 'user_inherit_default_permissions': False,
559 560 'explicit': False,
560 561 'algo': 'higherwin',
561 562 }
562 563 call_args.update(kwargs)
563 564 permissions = auth._cached_perms_data(**call_args)
564 565 return permissions
565 566
566 567
567 568 class TestGenerateAuthToken(object):
568 569 def test_salt_is_used_when_specified(self):
569 570 salt = 'abcde'
570 571 user_name = 'test_user'
571 572 result = auth.generate_auth_token(user_name, salt)
572 573 expected_result = sha1(user_name + salt).hexdigest()
573 574 assert result == expected_result
574 575
575 576 def test_salt_is_geneated_when_not_specified(self):
576 577 user_name = 'test_user'
577 578 random_salt = os.urandom(16)
578 579 with patch.object(auth, 'os') as os_mock:
579 580 os_mock.urandom.return_value = random_salt
580 581 result = auth.generate_auth_token(user_name)
581 582 expected_result = sha1(user_name + random_salt).hexdigest()
582 583 assert result == expected_result
584
585
586 @pytest.mark.parametrize("test_token, test_roles, auth_result, expected_tokens", [
587 ('', None, False,
588 []),
589 ('wrongtoken', None, False,
590 []),
591 ('abracadabra_vcs', [AuthTokenModel.cls.ROLE_API], False,
592 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
593 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
594 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
595 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
596 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1),
597 ('abracadabra_http', AuthTokenModel.cls.ROLE_HTTP, -1)]),
598 ])
599 def test_auth_by_token(test_token, test_roles, auth_result, expected_tokens,
600 user_util):
601 user = user_util.create_user()
602 user_id = user.user_id
603 for token, role, expires in expected_tokens:
604 new_token = AuthTokenModel().create(user_id, 'test-token', expires, role)
605 new_token.api_key = token # inject known name for testing...
606
607 assert auth_result == user.authenticate_by_token(
608 test_token, roles=test_roles, include_builtin_token=True)
General Comments 0
You need to be logged in to leave comments. Login now