##// END OF EJS Templates
core: fixed cython compat inspect that caused some API calls to not work correctly.
marcink -
r4184:ac8c6020 stable
parent child Browse files
Show More
@@ -1,554 +1,555 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 import inspect
22 21 import itertools
23 22 import logging
24 23 import sys
25 24 import types
26 25 import fnmatch
27 26
28 27 import decorator
29 28 import venusian
30 29 from collections import OrderedDict
31 30
32 31 from pyramid.exceptions import ConfigurationError
33 32 from pyramid.renderers import render
34 33 from pyramid.response import Response
35 34 from pyramid.httpexceptions import HTTPNotFound
36 35
37 36 from rhodecode.api.exc import (
38 37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
39 38 from rhodecode.apps._base import TemplateArgs
40 39 from rhodecode.lib.auth import AuthUser
41 40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
42 41 from rhodecode.lib.exc_tracking import store_exception
43 42 from rhodecode.lib.ext_json import json
44 43 from rhodecode.lib.utils2 import safe_str
45 44 from rhodecode.lib.plugins.utils import get_plugin_settings
46 45 from rhodecode.model.db import User, UserApiKeys
47 46
48 47 log = logging.getLogger(__name__)
49 48
50 49 DEFAULT_RENDERER = 'jsonrpc_renderer'
51 50 DEFAULT_URL = '/_admin/apiv2'
52 51
53 52
54 53 def find_methods(jsonrpc_methods, pattern):
55 54 matches = OrderedDict()
56 55 if not isinstance(pattern, (list, tuple)):
57 56 pattern = [pattern]
58 57
59 58 for single_pattern in pattern:
60 59 for method_name, method in jsonrpc_methods.items():
61 60 if fnmatch.fnmatch(method_name, single_pattern):
62 61 matches[method_name] = method
63 62 return matches
64 63
65 64
66 65 class ExtJsonRenderer(object):
67 66 """
68 67 Custom renderer that mkaes use of our ext_json lib
69 68
70 69 """
71 70
72 71 def __init__(self, serializer=json.dumps, **kw):
73 72 """ Any keyword arguments will be passed to the ``serializer``
74 73 function."""
75 74 self.serializer = serializer
76 75 self.kw = kw
77 76
78 77 def __call__(self, info):
79 78 """ Returns a plain JSON-encoded string with content-type
80 79 ``application/json``. The content-type may be overridden by
81 80 setting ``request.response.content_type``."""
82 81
83 82 def _render(value, system):
84 83 request = system.get('request')
85 84 if request is not None:
86 85 response = request.response
87 86 ct = response.content_type
88 87 if ct == response.default_content_type:
89 88 response.content_type = 'application/json'
90 89
91 90 return self.serializer(value, **self.kw)
92 91
93 92 return _render
94 93
95 94
96 95 def jsonrpc_response(request, result):
97 96 rpc_id = getattr(request, 'rpc_id', None)
98 97 response = request.response
99 98
100 99 # store content_type before render is called
101 100 ct = response.content_type
102 101
103 102 ret_value = ''
104 103 if rpc_id:
105 104 ret_value = {
106 105 'id': rpc_id,
107 106 'result': result,
108 107 'error': None,
109 108 }
110 109
111 110 # fetch deprecation warnings, and store it inside results
112 111 deprecation = getattr(request, 'rpc_deprecation', None)
113 112 if deprecation:
114 113 ret_value['DEPRECATION_WARNING'] = deprecation
115 114
116 115 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
117 116 response.body = safe_str(raw_body, response.charset)
118 117
119 118 if ct == response.default_content_type:
120 119 response.content_type = 'application/json'
121 120
122 121 return response
123 122
124 123
125 124 def jsonrpc_error(request, message, retid=None, code=None, headers=None):
126 125 """
127 126 Generate a Response object with a JSON-RPC error body
128 127
129 128 :param code:
130 129 :param retid:
131 130 :param message:
132 131 """
133 132 err_dict = {'id': retid, 'result': None, 'error': message}
134 133 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
135 134
136 135 return Response(
137 136 body=body,
138 137 status=code,
139 138 content_type='application/json',
140 139 headerlist=headers
141 140 )
142 141
143 142
144 143 def exception_view(exc, request):
145 144 rpc_id = getattr(request, 'rpc_id', None)
146 145
147 146 if isinstance(exc, JSONRPCError):
148 147 fault_message = safe_str(exc.message)
149 148 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
150 149 elif isinstance(exc, JSONRPCValidationError):
151 150 colander_exc = exc.colander_exception
152 151 # TODO(marcink): think maybe of nicer way to serialize errors ?
153 152 fault_message = colander_exc.asdict()
154 153 log.debug('json-rpc colander error rpc_id:%s "%s"', rpc_id, fault_message)
155 154 elif isinstance(exc, JSONRPCForbidden):
156 155 fault_message = 'Access was denied to this resource.'
157 156 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
158 157 elif isinstance(exc, HTTPNotFound):
159 158 method = request.rpc_method
160 159 log.debug('json-rpc method `%s` not found in list of '
161 160 'api calls: %s, rpc_id:%s',
162 161 method, request.registry.jsonrpc_methods.keys(), rpc_id)
163 162
164 163 similar = 'none'
165 164 try:
166 165 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
167 166 similar_found = find_methods(
168 167 request.registry.jsonrpc_methods, similar_paterns)
169 168 similar = ', '.join(similar_found.keys()) or similar
170 169 except Exception:
171 170 # make the whole above block safe
172 171 pass
173 172
174 173 fault_message = "No such method: {}. Similar methods: {}".format(
175 174 method, similar)
176 175 else:
177 176 fault_message = 'undefined error'
178 177 exc_info = exc.exc_info()
179 178 store_exception(id(exc_info), exc_info, prefix='rhodecode-api')
180 179
181 180 return jsonrpc_error(request, fault_message, rpc_id)
182 181
183 182
184 183 def request_view(request):
185 184 """
186 185 Main request handling method. It handles all logic to call a specific
187 186 exposed method
188 187 """
188 # cython compatible inspect
189 from rhodecode.config.patches import inspect_getargspec
190 inspect = inspect_getargspec()
189 191
190 192 # check if we can find this session using api_key, get_by_auth_token
191 193 # search not expired tokens only
192
193 194 try:
194 195 api_user = User.get_by_auth_token(request.rpc_api_key)
195 196
196 197 if api_user is None:
197 198 return jsonrpc_error(
198 199 request, retid=request.rpc_id, message='Invalid API KEY')
199 200
200 201 if not api_user.active:
201 202 return jsonrpc_error(
202 203 request, retid=request.rpc_id,
203 204 message='Request from this user not allowed')
204 205
205 206 # check if we are allowed to use this IP
206 207 auth_u = AuthUser(
207 208 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
208 209 if not auth_u.ip_allowed:
209 210 return jsonrpc_error(
210 211 request, retid=request.rpc_id,
211 212 message='Request from IP:%s not allowed' % (
212 213 request.rpc_ip_addr,))
213 214 else:
214 215 log.info('Access for IP:%s allowed', request.rpc_ip_addr)
215 216
216 217 # register our auth-user
217 218 request.rpc_user = auth_u
218 219 request.environ['rc_auth_user_id'] = auth_u.user_id
219 220
220 221 # now check if token is valid for API
221 222 auth_token = request.rpc_api_key
222 223 token_match = api_user.authenticate_by_token(
223 224 auth_token, roles=[UserApiKeys.ROLE_API])
224 225 invalid_token = not token_match
225 226
226 227 log.debug('Checking if API KEY is valid with proper role')
227 228 if invalid_token:
228 229 return jsonrpc_error(
229 230 request, retid=request.rpc_id,
230 231 message='API KEY invalid or, has bad role for an API call')
231 232
232 233 except Exception:
233 234 log.exception('Error on API AUTH')
234 235 return jsonrpc_error(
235 236 request, retid=request.rpc_id, message='Invalid API KEY')
236 237
237 238 method = request.rpc_method
238 239 func = request.registry.jsonrpc_methods[method]
239 240
240 241 # now that we have a method, add request._req_params to
241 242 # self.kargs and dispatch control to WGIController
242 243 argspec = inspect.getargspec(func)
243 244 arglist = argspec[0]
244 245 defaults = map(type, argspec[3] or [])
245 246 default_empty = types.NotImplementedType
246 247
247 248 # kw arguments required by this method
248 249 func_kwargs = dict(itertools.izip_longest(
249 250 reversed(arglist), reversed(defaults), fillvalue=default_empty))
250 251
251 252 # This attribute will need to be first param of a method that uses
252 253 # api_key, which is translated to instance of user at that name
253 254 user_var = 'apiuser'
254 255 request_var = 'request'
255 256
256 257 for arg in [user_var, request_var]:
257 258 if arg not in arglist:
258 259 return jsonrpc_error(
259 260 request,
260 261 retid=request.rpc_id,
261 262 message='This method [%s] does not support '
262 263 'required parameter `%s`' % (func.__name__, arg))
263 264
264 265 # get our arglist and check if we provided them as args
265 266 for arg, default in func_kwargs.items():
266 267 if arg in [user_var, request_var]:
267 268 # user_var and request_var are pre-hardcoded parameters and we
268 269 # don't need to do any translation
269 270 continue
270 271
271 272 # skip the required param check if it's default value is
272 273 # NotImplementedType (default_empty)
273 274 if default == default_empty and arg not in request.rpc_params:
274 275 return jsonrpc_error(
275 276 request,
276 277 retid=request.rpc_id,
277 278 message=('Missing non optional `%s` arg in JSON DATA' % arg)
278 279 )
279 280
280 281 # sanitize extra passed arguments
281 282 for k in request.rpc_params.keys()[:]:
282 283 if k not in func_kwargs:
283 284 del request.rpc_params[k]
284 285
285 286 call_params = request.rpc_params
286 287 call_params.update({
287 288 'request': request,
288 289 'apiuser': auth_u
289 290 })
290 291
291 292 # register some common functions for usage
292 293 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id)
293 294
294 295 try:
295 296 ret_value = func(**call_params)
296 297 return jsonrpc_response(request, ret_value)
297 298 except JSONRPCBaseError:
298 299 raise
299 300 except Exception:
300 301 log.exception('Unhandled exception occurred on api call: %s', func)
301 302 exc_info = sys.exc_info()
302 303 exc_id, exc_type_name = store_exception(
303 304 id(exc_info), exc_info, prefix='rhodecode-api')
304 305 error_headers = [('RhodeCode-Exception-Id', str(exc_id)),
305 306 ('RhodeCode-Exception-Type', str(exc_type_name))]
306 307 return jsonrpc_error(
307 308 request, retid=request.rpc_id, message='Internal server error',
308 309 headers=error_headers)
309 310
310 311
311 312 def setup_request(request):
312 313 """
313 314 Parse a JSON-RPC request body. It's used inside the predicates method
314 315 to validate and bootstrap requests for usage in rpc calls.
315 316
316 317 We need to raise JSONRPCError here if we want to return some errors back to
317 318 user.
318 319 """
319 320
320 321 log.debug('Executing setup request: %r', request)
321 322 request.rpc_ip_addr = get_ip_addr(request.environ)
322 323 # TODO(marcink): deprecate GET at some point
323 324 if request.method not in ['POST', 'GET']:
324 325 log.debug('unsupported request method "%s"', request.method)
325 326 raise JSONRPCError(
326 327 'unsupported request method "%s". Please use POST' % request.method)
327 328
328 329 if 'CONTENT_LENGTH' not in request.environ:
329 330 log.debug("No Content-Length")
330 331 raise JSONRPCError("Empty body, No Content-Length in request")
331 332
332 333 else:
333 334 length = request.environ['CONTENT_LENGTH']
334 335 log.debug('Content-Length: %s', length)
335 336
336 337 if length == 0:
337 338 log.debug("Content-Length is 0")
338 339 raise JSONRPCError("Content-Length is 0")
339 340
340 341 raw_body = request.body
341 342 log.debug("Loading JSON body now")
342 343 try:
343 344 json_body = json.loads(raw_body)
344 345 except ValueError as e:
345 346 # catch JSON errors Here
346 347 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
347 348
348 349 request.rpc_id = json_body.get('id')
349 350 request.rpc_method = json_body.get('method')
350 351
351 352 # check required base parameters
352 353 try:
353 354 api_key = json_body.get('api_key')
354 355 if not api_key:
355 356 api_key = json_body.get('auth_token')
356 357
357 358 if not api_key:
358 359 raise KeyError('api_key or auth_token')
359 360
360 361 # TODO(marcink): support passing in token in request header
361 362
362 363 request.rpc_api_key = api_key
363 364 request.rpc_id = json_body['id']
364 365 request.rpc_method = json_body['method']
365 366 request.rpc_params = json_body['args'] \
366 367 if isinstance(json_body['args'], dict) else {}
367 368
368 369 log.debug('method: %s, params: %.10240r', request.rpc_method, request.rpc_params)
369 370 except KeyError as e:
370 371 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
371 372
372 373 log.debug('setup complete, now handling method:%s rpcid:%s',
373 374 request.rpc_method, request.rpc_id, )
374 375
375 376
376 377 class RoutePredicate(object):
377 378 def __init__(self, val, config):
378 379 self.val = val
379 380
380 381 def text(self):
381 382 return 'jsonrpc route = %s' % self.val
382 383
383 384 phash = text
384 385
385 386 def __call__(self, info, request):
386 387 if self.val:
387 388 # potentially setup and bootstrap our call
388 389 setup_request(request)
389 390
390 391 # Always return True so that even if it isn't a valid RPC it
391 392 # will fall through to the underlaying handlers like notfound_view
392 393 return True
393 394
394 395
395 396 class NotFoundPredicate(object):
396 397 def __init__(self, val, config):
397 398 self.val = val
398 399 self.methods = config.registry.jsonrpc_methods
399 400
400 401 def text(self):
401 402 return 'jsonrpc method not found = {}.'.format(self.val)
402 403
403 404 phash = text
404 405
405 406 def __call__(self, info, request):
406 407 return hasattr(request, 'rpc_method')
407 408
408 409
409 410 class MethodPredicate(object):
410 411 def __init__(self, val, config):
411 412 self.method = val
412 413
413 414 def text(self):
414 415 return 'jsonrpc method = %s' % self.method
415 416
416 417 phash = text
417 418
418 419 def __call__(self, context, request):
419 420 # we need to explicitly return False here, so pyramid doesn't try to
420 421 # execute our view directly. We need our main handler to execute things
421 422 return getattr(request, 'rpc_method') == self.method
422 423
423 424
424 425 def add_jsonrpc_method(config, view, **kwargs):
425 426 # pop the method name
426 427 method = kwargs.pop('method', None)
427 428
428 429 if method is None:
429 430 raise ConfigurationError(
430 431 'Cannot register a JSON-RPC method without specifying the "method"')
431 432
432 433 # we define custom predicate, to enable to detect conflicting methods,
433 434 # those predicates are kind of "translation" from the decorator variables
434 435 # to internal predicates names
435 436
436 437 kwargs['jsonrpc_method'] = method
437 438
438 439 # register our view into global view store for validation
439 440 config.registry.jsonrpc_methods[method] = view
440 441
441 442 # we're using our main request_view handler, here, so each method
442 443 # has a unified handler for itself
443 444 config.add_view(request_view, route_name='apiv2', **kwargs)
444 445
445 446
446 447 class jsonrpc_method(object):
447 448 """
448 449 decorator that works similar to @add_view_config decorator,
449 450 but tailored for our JSON RPC
450 451 """
451 452
452 453 venusian = venusian # for testing injection
453 454
454 455 def __init__(self, method=None, **kwargs):
455 456 self.method = method
456 457 self.kwargs = kwargs
457 458
458 459 def __call__(self, wrapped):
459 460 kwargs = self.kwargs.copy()
460 461 kwargs['method'] = self.method or wrapped.__name__
461 462 depth = kwargs.pop('_depth', 0)
462 463
463 464 def callback(context, name, ob):
464 465 config = context.config.with_package(info.module)
465 466 config.add_jsonrpc_method(view=ob, **kwargs)
466 467
467 468 info = venusian.attach(wrapped, callback, category='pyramid',
468 469 depth=depth + 1)
469 470 if info.scope == 'class':
470 471 # ensure that attr is set if decorating a class method
471 472 kwargs.setdefault('attr', wrapped.__name__)
472 473
473 474 kwargs['_info'] = info.codeinfo # fbo action_method
474 475 return wrapped
475 476
476 477
477 478 class jsonrpc_deprecated_method(object):
478 479 """
479 480 Marks method as deprecated, adds log.warning, and inject special key to
480 481 the request variable to mark method as deprecated.
481 482 Also injects special docstring that extract_docs will catch to mark
482 483 method as deprecated.
483 484
484 485 :param use_method: specify which method should be used instead of
485 486 the decorated one
486 487
487 488 Use like::
488 489
489 490 @jsonrpc_method()
490 491 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
491 492 def old_func(request, apiuser, arg1, arg2):
492 493 ...
493 494 """
494 495
495 496 def __init__(self, use_method, deprecated_at_version):
496 497 self.use_method = use_method
497 498 self.deprecated_at_version = deprecated_at_version
498 499 self.deprecated_msg = ''
499 500
500 501 def __call__(self, func):
501 502 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
502 503 method=self.use_method)
503 504
504 505 docstring = """\n
505 506 .. deprecated:: {version}
506 507
507 508 {deprecation_message}
508 509
509 510 {original_docstring}
510 511 """
511 512 func.__doc__ = docstring.format(
512 513 version=self.deprecated_at_version,
513 514 deprecation_message=self.deprecated_msg,
514 515 original_docstring=func.__doc__)
515 516 return decorator.decorator(self.__wrapper, func)
516 517
517 518 def __wrapper(self, func, *fargs, **fkwargs):
518 519 log.warning('DEPRECATED API CALL on function %s, please '
519 520 'use `%s` instead', func, self.use_method)
520 521 # alter function docstring to mark as deprecated, this is picked up
521 522 # via fabric file that generates API DOC.
522 523 result = func(*fargs, **fkwargs)
523 524
524 525 request = fargs[0]
525 526 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
526 527 return result
527 528
528 529
529 530 def includeme(config):
530 531 plugin_module = 'rhodecode.api'
531 532 plugin_settings = get_plugin_settings(
532 533 plugin_module, config.registry.settings)
533 534
534 535 if not hasattr(config.registry, 'jsonrpc_methods'):
535 536 config.registry.jsonrpc_methods = OrderedDict()
536 537
537 538 # match filter by given method only
538 539 config.add_view_predicate('jsonrpc_method', MethodPredicate)
539 540 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
540 541
541 542 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
542 543 serializer=json.dumps, indent=4))
543 544 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
544 545
545 546 config.add_route_predicate(
546 547 'jsonrpc_call', RoutePredicate)
547 548
548 549 config.add_route(
549 550 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
550 551
551 552 config.scan(plugin_module, ignore='rhodecode.api.tests')
552 553 # register some exception handling view
553 554 config.add_view(exception_view, context=JSONRPCBaseError)
554 555 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,419 +1,421 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2019 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 import inspect
22 21 import logging
23 22 import itertools
24 23 import base64
25 24
26 25 from pyramid import compat
27 26
28 27 from rhodecode.api import (
29 28 jsonrpc_method, JSONRPCError, JSONRPCForbidden, find_methods)
30 29
31 30 from rhodecode.api.utils import (
32 31 Optional, OAttr, has_superadmin_permission, get_user_or_error)
33 32 from rhodecode.lib.utils import repo2db_mapper
34 33 from rhodecode.lib import system_info
35 34 from rhodecode.lib import user_sessions
36 35 from rhodecode.lib import exc_tracking
37 36 from rhodecode.lib.ext_json import json
38 37 from rhodecode.lib.utils2 import safe_int
39 38 from rhodecode.model.db import UserIpMap
40 39 from rhodecode.model.scm import ScmModel
41 40 from rhodecode.model.settings import VcsSettingsModel
42 41 from rhodecode.apps.file_store import utils
43 42 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
44 43 FileOverSizeException
45 44
46 45 log = logging.getLogger(__name__)
47 46
48 47
49 48 @jsonrpc_method()
50 49 def get_server_info(request, apiuser):
51 50 """
52 51 Returns the |RCE| server information.
53 52
54 53 This includes the running version of |RCE| and all installed
55 54 packages. This command takes the following options:
56 55
57 56 :param apiuser: This is filled automatically from the |authtoken|.
58 57 :type apiuser: AuthUser
59 58
60 59 Example output:
61 60
62 61 .. code-block:: bash
63 62
64 63 id : <id_given_in_input>
65 64 result : {
66 65 'modules': [<module name>,...]
67 66 'py_version': <python version>,
68 67 'platform': <platform type>,
69 68 'rhodecode_version': <rhodecode version>
70 69 }
71 70 error : null
72 71 """
73 72
74 73 if not has_superadmin_permission(apiuser):
75 74 raise JSONRPCForbidden()
76 75
77 76 server_info = ScmModel().get_server_info(request.environ)
78 77 # rhodecode-index requires those
79 78
80 79 server_info['index_storage'] = server_info['search']['value']['location']
81 80 server_info['storage'] = server_info['storage']['value']['path']
82 81
83 82 return server_info
84 83
85 84
86 85 @jsonrpc_method()
87 86 def get_repo_store(request, apiuser):
88 87 """
89 88 Returns the |RCE| repository storage information.
90 89
91 90 :param apiuser: This is filled automatically from the |authtoken|.
92 91 :type apiuser: AuthUser
93 92
94 93 Example output:
95 94
96 95 .. code-block:: bash
97 96
98 97 id : <id_given_in_input>
99 98 result : {
100 99 'modules': [<module name>,...]
101 100 'py_version': <python version>,
102 101 'platform': <platform type>,
103 102 'rhodecode_version': <rhodecode version>
104 103 }
105 104 error : null
106 105 """
107 106
108 107 if not has_superadmin_permission(apiuser):
109 108 raise JSONRPCForbidden()
110 109
111 110 path = VcsSettingsModel().get_repos_location()
112 111 return {"path": path}
113 112
114 113
115 114 @jsonrpc_method()
116 115 def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
117 116 """
118 117 Displays the IP Address as seen from the |RCE| server.
119 118
120 119 * This command displays the IP Address, as well as all the defined IP
121 120 addresses for the specified user. If the ``userid`` is not set, the
122 121 data returned is for the user calling the method.
123 122
124 123 This command can only be run using an |authtoken| with admin rights to
125 124 the specified repository.
126 125
127 126 This command takes the following options:
128 127
129 128 :param apiuser: This is filled automatically from |authtoken|.
130 129 :type apiuser: AuthUser
131 130 :param userid: Sets the userid for which associated IP Address data
132 131 is returned.
133 132 :type userid: Optional(str or int)
134 133
135 134 Example output:
136 135
137 136 .. code-block:: bash
138 137
139 138 id : <id_given_in_input>
140 139 result : {
141 140 "server_ip_addr": "<ip_from_clien>",
142 141 "user_ips": [
143 142 {
144 143 "ip_addr": "<ip_with_mask>",
145 144 "ip_range": ["<start_ip>", "<end_ip>"],
146 145 },
147 146 ...
148 147 ]
149 148 }
150 149
151 150 """
152 151 if not has_superadmin_permission(apiuser):
153 152 raise JSONRPCForbidden()
154 153
155 154 userid = Optional.extract(userid, evaluate_locals=locals())
156 155 userid = getattr(userid, 'user_id', userid)
157 156
158 157 user = get_user_or_error(userid)
159 158 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
160 159 return {
161 160 'server_ip_addr': request.rpc_ip_addr,
162 161 'user_ips': ips
163 162 }
164 163
165 164
166 165 @jsonrpc_method()
167 166 def rescan_repos(request, apiuser, remove_obsolete=Optional(False)):
168 167 """
169 168 Triggers a rescan of the specified repositories.
170 169
171 170 * If the ``remove_obsolete`` option is set, it also deletes repositories
172 171 that are found in the database but not on the file system, so called
173 172 "clean zombies".
174 173
175 174 This command can only be run using an |authtoken| with admin rights to
176 175 the specified repository.
177 176
178 177 This command takes the following options:
179 178
180 179 :param apiuser: This is filled automatically from the |authtoken|.
181 180 :type apiuser: AuthUser
182 181 :param remove_obsolete: Deletes repositories from the database that
183 182 are not found on the filesystem.
184 183 :type remove_obsolete: Optional(``True`` | ``False``)
185 184
186 185 Example output:
187 186
188 187 .. code-block:: bash
189 188
190 189 id : <id_given_in_input>
191 190 result : {
192 191 'added': [<added repository name>,...]
193 192 'removed': [<removed repository name>,...]
194 193 }
195 194 error : null
196 195
197 196 Example error output:
198 197
199 198 .. code-block:: bash
200 199
201 200 id : <id_given_in_input>
202 201 result : null
203 202 error : {
204 203 'Error occurred during rescan repositories action'
205 204 }
206 205
207 206 """
208 207 if not has_superadmin_permission(apiuser):
209 208 raise JSONRPCForbidden()
210 209
211 210 try:
212 211 rm_obsolete = Optional.extract(remove_obsolete)
213 212 added, removed = repo2db_mapper(ScmModel().repo_scan(),
214 213 remove_obsolete=rm_obsolete)
215 214 return {'added': added, 'removed': removed}
216 215 except Exception:
217 216 log.exception('Failed to run repo rescann')
218 217 raise JSONRPCError(
219 218 'Error occurred during rescan repositories action'
220 219 )
221 220
222 221
223 222 @jsonrpc_method()
224 223 def cleanup_sessions(request, apiuser, older_then=Optional(60)):
225 224 """
226 225 Triggers a session cleanup action.
227 226
228 227 If the ``older_then`` option is set, only sessions that hasn't been
229 228 accessed in the given number of days will be removed.
230 229
231 230 This command can only be run using an |authtoken| with admin rights to
232 231 the specified repository.
233 232
234 233 This command takes the following options:
235 234
236 235 :param apiuser: This is filled automatically from the |authtoken|.
237 236 :type apiuser: AuthUser
238 237 :param older_then: Deletes session that hasn't been accessed
239 238 in given number of days.
240 239 :type older_then: Optional(int)
241 240
242 241 Example output:
243 242
244 243 .. code-block:: bash
245 244
246 245 id : <id_given_in_input>
247 246 result: {
248 247 "backend": "<type of backend>",
249 248 "sessions_removed": <number_of_removed_sessions>
250 249 }
251 250 error : null
252 251
253 252 Example error output:
254 253
255 254 .. code-block:: bash
256 255
257 256 id : <id_given_in_input>
258 257 result : null
259 258 error : {
260 259 'Error occurred during session cleanup'
261 260 }
262 261
263 262 """
264 263 if not has_superadmin_permission(apiuser):
265 264 raise JSONRPCForbidden()
266 265
267 266 older_then = safe_int(Optional.extract(older_then)) or 60
268 267 older_than_seconds = 60 * 60 * 24 * older_then
269 268
270 269 config = system_info.rhodecode_config().get_value()['value']['config']
271 270 session_model = user_sessions.get_session_handler(
272 271 config.get('beaker.session.type', 'memory'))(config)
273 272
274 273 backend = session_model.SESSION_TYPE
275 274 try:
276 275 cleaned = session_model.clean_sessions(
277 276 older_than_seconds=older_than_seconds)
278 277 return {'sessions_removed': cleaned, 'backend': backend}
279 278 except user_sessions.CleanupCommand as msg:
280 279 return {'cleanup_command': msg.message, 'backend': backend}
281 280 except Exception as e:
282 281 log.exception('Failed session cleanup')
283 282 raise JSONRPCError(
284 283 'Error occurred during session cleanup'
285 284 )
286 285
287 286
288 287 @jsonrpc_method()
289 288 def get_method(request, apiuser, pattern=Optional('*')):
290 289 """
291 290 Returns list of all available API methods. By default match pattern
292 291 os "*" but any other pattern can be specified. eg *comment* will return
293 292 all methods with comment inside them. If just single method is matched
294 293 returned data will also include method specification
295 294
296 295 This command can only be run using an |authtoken| with admin rights to
297 296 the specified repository.
298 297
299 298 This command takes the following options:
300 299
301 300 :param apiuser: This is filled automatically from the |authtoken|.
302 301 :type apiuser: AuthUser
303 302 :param pattern: pattern to match method names against
304 303 :type pattern: Optional("*")
305 304
306 305 Example output:
307 306
308 307 .. code-block:: bash
309 308
310 309 id : <id_given_in_input>
311 310 "result": [
312 311 "changeset_comment",
313 312 "comment_pull_request",
314 313 "comment_commit"
315 314 ]
316 315 error : null
317 316
318 317 .. code-block:: bash
319 318
320 319 id : <id_given_in_input>
321 320 "result": [
322 321 "comment_commit",
323 322 {
324 323 "apiuser": "<RequiredType>",
325 324 "comment_type": "<Optional:u'note'>",
326 325 "commit_id": "<RequiredType>",
327 326 "message": "<RequiredType>",
328 327 "repoid": "<RequiredType>",
329 328 "request": "<RequiredType>",
330 329 "resolves_comment_id": "<Optional:None>",
331 330 "status": "<Optional:None>",
332 331 "userid": "<Optional:<OptionalAttr:apiuser>>"
333 332 }
334 333 ]
335 334 error : null
336 335 """
336 from rhodecode.config.patches import inspect_getargspec
337 inspect = inspect_getargspec()
338
337 339 if not has_superadmin_permission(apiuser):
338 340 raise JSONRPCForbidden()
339 341
340 342 pattern = Optional.extract(pattern)
341 343
342 344 matches = find_methods(request.registry.jsonrpc_methods, pattern)
343 345
344 346 args_desc = []
345 347 if len(matches) == 1:
346 348 func = matches[matches.keys()[0]]
347 349
348 350 argspec = inspect.getargspec(func)
349 351 arglist = argspec[0]
350 352 defaults = map(repr, argspec[3] or [])
351 353
352 354 default_empty = '<RequiredType>'
353 355
354 356 # kw arguments required by this method
355 357 func_kwargs = dict(itertools.izip_longest(
356 358 reversed(arglist), reversed(defaults), fillvalue=default_empty))
357 359 args_desc.append(func_kwargs)
358 360
359 361 return matches.keys() + args_desc
360 362
361 363
362 364 @jsonrpc_method()
363 365 def store_exception(request, apiuser, exc_data_json, prefix=Optional('rhodecode')):
364 366 """
365 367 Stores sent exception inside the built-in exception tracker in |RCE| server.
366 368
367 369 This command can only be run using an |authtoken| with admin rights to
368 370 the specified repository.
369 371
370 372 This command takes the following options:
371 373
372 374 :param apiuser: This is filled automatically from the |authtoken|.
373 375 :type apiuser: AuthUser
374 376
375 377 :param exc_data_json: JSON data with exception e.g
376 378 {"exc_traceback": "Value `1` is not allowed", "exc_type_name": "ValueError"}
377 379 :type exc_data_json: JSON data
378 380
379 381 :param prefix: prefix for error type, e.g 'rhodecode', 'vcsserver', 'rhodecode-tools'
380 382 :type prefix: Optional("rhodecode")
381 383
382 384 Example output:
383 385
384 386 .. code-block:: bash
385 387
386 388 id : <id_given_in_input>
387 389 "result": {
388 390 "exc_id": 139718459226384,
389 391 "exc_url": "http://localhost:8080/_admin/settings/exceptions/139718459226384"
390 392 }
391 393 error : null
392 394 """
393 395 if not has_superadmin_permission(apiuser):
394 396 raise JSONRPCForbidden()
395 397
396 398 prefix = Optional.extract(prefix)
397 399 exc_id = exc_tracking.generate_id()
398 400
399 401 try:
400 402 exc_data = json.loads(exc_data_json)
401 403 except Exception:
402 404 log.error('Failed to parse JSON: %r', exc_data_json)
403 405 raise JSONRPCError('Failed to parse JSON data from exc_data_json field. '
404 406 'Please make sure it contains a valid JSON.')
405 407
406 408 try:
407 409 exc_traceback = exc_data['exc_traceback']
408 410 exc_type_name = exc_data['exc_type_name']
409 411 except KeyError as err:
410 412 raise JSONRPCError('Missing exc_traceback, or exc_type_name '
411 413 'in exc_data_json field. Missing: {}'.format(err))
412 414
413 415 exc_tracking._store_exception(
414 416 exc_id=exc_id, exc_traceback=exc_traceback,
415 417 exc_type_name=exc_type_name, prefix=prefix)
416 418
417 419 exc_url = request.route_url(
418 420 'admin_settings_exception_tracker_show', exception_id=exc_id)
419 421 return {'exc_id': exc_id, 'exc_url': exc_url}
@@ -1,97 +1,99 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 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 Compatibility patches.
23 23
24 24 Please keep the following principles in mind:
25 25
26 26 * Keep imports local, so that importing this module does not cause too many
27 27 side effects by itself.
28 28
29 29 * Try to make patches idempotent, calling them multiple times should not do
30 30 harm. If that is not possible, ensure that the second call explodes.
31 31
32 32 """
33 33
34 34
35 35 def inspect_getargspec():
36 36 """
37 37 Pyramid rely on inspect.getargspec to lookup the signature of
38 38 view functions. This is not compatible with cython, therefore we replace
39 39 getargspec with a custom version.
40 40 Code is inspired by the inspect module from Python-3.4
41 41 """
42 42 import inspect
43 43
44 44 def _isCython(func):
45 45 """
46 46 Private helper that checks if a function is a cython function.
47 47 """
48 48 return func.__class__.__name__ == 'cython_function_or_method'
49 49
50 50 def unwrap(func):
51 51 """
52 52 Get the object wrapped by *func*.
53 53
54 54 Follows the chain of :attr:`__wrapped__` attributes returning the last
55 55 object in the chain.
56 56
57 57 *stop* is an optional callback accepting an object in the wrapper chain
58 58 as its sole argument that allows the unwrapping to be terminated early
59 59 if the callback returns a true value. If the callback never returns a
60 60 true value, the last object in the chain is returned as usual. For
61 61 example, :func:`signature` uses this to stop unwrapping if any object
62 62 in the chain has a ``__signature__`` attribute defined.
63 63
64 64 :exc:`ValueError` is raised if a cycle is encountered.
65 65 """
66 66 f = func # remember the original func for error reporting
67 67 memo = {id(f)} # Memoise by id to tolerate non-hashable objects
68 68 while hasattr(func, '__wrapped__'):
69 69 func = func.__wrapped__
70 70 id_func = id(func)
71 71 if id_func in memo:
72 72 raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
73 73 memo.add(id_func)
74 74 return func
75 75
76 76 def custom_getargspec(func):
77 77 """
78 78 Get the names and default values of a function's arguments.
79 79
80 80 A tuple of four things is returned: (args, varargs, varkw, defaults).
81 81 'args' is a list of the argument names (it may contain nested lists).
82 82 'varargs' and 'varkw' are the names of the * and ** arguments or None.
83 83 'defaults' is an n-tuple of the default values of the last n arguments.
84 84 """
85 85
86 86 func = unwrap(func)
87 87
88 88 if inspect.ismethod(func):
89 89 func = func.im_func
90 90 if not inspect.isfunction(func):
91 91 if not _isCython(func):
92 92 raise TypeError('{!r} is not a Python or Cython function'
93 93 .format(func))
94 94 args, varargs, varkw = inspect.getargs(func.func_code)
95 95 return inspect.ArgSpec(args, varargs, varkw, func.func_defaults)
96 96
97 97 inspect.getargspec = custom_getargspec
98
99 return inspect
@@ -1,2413 +1,2413 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 authentication and permission libraries
23 23 """
24 24
25 25 import os
26 26 import time
27 import inspect
28 27 import collections
29 28 import fnmatch
30 29 import hashlib
31 30 import itertools
32 31 import logging
33 32 import random
34 33 import traceback
35 34 from functools import wraps
36 35
37 36 import ipaddress
38 37
39 38 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
40 39 from sqlalchemy.orm.exc import ObjectDeletedError
41 40 from sqlalchemy.orm import joinedload
42 41 from zope.cachedescriptors.property import Lazy as LazyProperty
43 42
44 43 import rhodecode
45 44 from rhodecode.model import meta
46 45 from rhodecode.model.meta import Session
47 46 from rhodecode.model.user import UserModel
48 47 from rhodecode.model.db import (
49 48 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
50 49 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
51 50 from rhodecode.lib import rc_cache
52 51 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
53 52 from rhodecode.lib.utils import (
54 53 get_repo_slug, get_repo_group_slug, get_user_group_slug)
55 54 from rhodecode.lib.caching_query import FromCache
56 55
57 56
58 57 if rhodecode.is_unix:
59 58 import bcrypt
60 59
61 60 log = logging.getLogger(__name__)
62 61
63 62 csrf_token_key = "csrf_token"
64 63
65 64
66 65 class PasswordGenerator(object):
67 66 """
68 67 This is a simple class for generating password from different sets of
69 68 characters
70 69 usage::
71 70 passwd_gen = PasswordGenerator()
72 71 #print 8-letter password containing only big and small letters
73 72 of alphabet
74 73 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 74 """
76 75 ALPHABETS_NUM = r'''1234567890'''
77 76 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 77 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 78 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 79 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 80 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 81 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 82 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 83 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 84 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86 85
87 86 def __init__(self, passwd=''):
88 87 self.passwd = passwd
89 88
90 89 def gen_password(self, length, type_=None):
91 90 if type_ is None:
92 91 type_ = self.ALPHABETS_FULL
93 92 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
94 93 return self.passwd
95 94
96 95
97 96 class _RhodeCodeCryptoBase(object):
98 97 ENC_PREF = None
99 98
100 99 def hash_create(self, str_):
101 100 """
102 101 hash the string using
103 102
104 103 :param str_: password to hash
105 104 """
106 105 raise NotImplementedError
107 106
108 107 def hash_check_with_upgrade(self, password, hashed):
109 108 """
110 109 Returns tuple in which first element is boolean that states that
111 110 given password matches it's hashed version, and the second is new hash
112 111 of the password, in case this password should be migrated to new
113 112 cipher.
114 113 """
115 114 checked_hash = self.hash_check(password, hashed)
116 115 return checked_hash, None
117 116
118 117 def hash_check(self, password, hashed):
119 118 """
120 119 Checks matching password with it's hashed value.
121 120
122 121 :param password: password
123 122 :param hashed: password in hashed form
124 123 """
125 124 raise NotImplementedError
126 125
127 126 def _assert_bytes(self, value):
128 127 """
129 128 Passing in an `unicode` object can lead to hard to detect issues
130 129 if passwords contain non-ascii characters. Doing a type check
131 130 during runtime, so that such mistakes are detected early on.
132 131 """
133 132 if not isinstance(value, str):
134 133 raise TypeError(
135 134 "Bytestring required as input, got %r." % (value, ))
136 135
137 136
138 137 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
139 138 ENC_PREF = ('$2a$10', '$2b$10')
140 139
141 140 def hash_create(self, str_):
142 141 self._assert_bytes(str_)
143 142 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
144 143
145 144 def hash_check_with_upgrade(self, password, hashed):
146 145 """
147 146 Returns tuple in which first element is boolean that states that
148 147 given password matches it's hashed version, and the second is new hash
149 148 of the password, in case this password should be migrated to new
150 149 cipher.
151 150
152 151 This implements special upgrade logic which works like that:
153 152 - check if the given password == bcrypted hash, if yes then we
154 153 properly used password and it was already in bcrypt. Proceed
155 154 without any changes
156 155 - if bcrypt hash check is not working try with sha256. If hash compare
157 156 is ok, it means we using correct but old hashed password. indicate
158 157 hash change and proceed
159 158 """
160 159
161 160 new_hash = None
162 161
163 162 # regular pw check
164 163 password_match_bcrypt = self.hash_check(password, hashed)
165 164
166 165 # now we want to know if the password was maybe from sha256
167 166 # basically calling _RhodeCodeCryptoSha256().hash_check()
168 167 if not password_match_bcrypt:
169 168 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
170 169 new_hash = self.hash_create(password) # make new bcrypt hash
171 170 password_match_bcrypt = True
172 171
173 172 return password_match_bcrypt, new_hash
174 173
175 174 def hash_check(self, password, hashed):
176 175 """
177 176 Checks matching password with it's hashed value.
178 177
179 178 :param password: password
180 179 :param hashed: password in hashed form
181 180 """
182 181 self._assert_bytes(password)
183 182 try:
184 183 return bcrypt.hashpw(password, hashed) == hashed
185 184 except ValueError as e:
186 185 # we're having a invalid salt here probably, we should not crash
187 186 # just return with False as it would be a wrong password.
188 187 log.debug('Failed to check password hash using bcrypt %s',
189 188 safe_str(e))
190 189
191 190 return False
192 191
193 192
194 193 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
195 194 ENC_PREF = '_'
196 195
197 196 def hash_create(self, str_):
198 197 self._assert_bytes(str_)
199 198 return hashlib.sha256(str_).hexdigest()
200 199
201 200 def hash_check(self, password, hashed):
202 201 """
203 202 Checks matching password with it's hashed value.
204 203
205 204 :param password: password
206 205 :param hashed: password in hashed form
207 206 """
208 207 self._assert_bytes(password)
209 208 return hashlib.sha256(password).hexdigest() == hashed
210 209
211 210
212 211 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
213 212 ENC_PREF = '_'
214 213
215 214 def hash_create(self, str_):
216 215 self._assert_bytes(str_)
217 216 return sha1(str_)
218 217
219 218 def hash_check(self, password, hashed):
220 219 """
221 220 Checks matching password with it's hashed value.
222 221
223 222 :param password: password
224 223 :param hashed: password in hashed form
225 224 """
226 225 self._assert_bytes(password)
227 226 return sha1(password) == hashed
228 227
229 228
230 229 def crypto_backend():
231 230 """
232 231 Return the matching crypto backend.
233 232
234 233 Selection is based on if we run tests or not, we pick sha1-test backend to run
235 234 tests faster since BCRYPT is expensive to calculate
236 235 """
237 236 if rhodecode.is_test:
238 237 RhodeCodeCrypto = _RhodeCodeCryptoTest()
239 238 else:
240 239 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 240
242 241 return RhodeCodeCrypto
243 242
244 243
245 244 def get_crypt_password(password):
246 245 """
247 246 Create the hash of `password` with the active crypto backend.
248 247
249 248 :param password: The cleartext password.
250 249 :type password: unicode
251 250 """
252 251 password = safe_str(password)
253 252 return crypto_backend().hash_create(password)
254 253
255 254
256 255 def check_password(password, hashed):
257 256 """
258 257 Check if the value in `password` matches the hash in `hashed`.
259 258
260 259 :param password: The cleartext password.
261 260 :type password: unicode
262 261
263 262 :param hashed: The expected hashed version of the password.
264 263 :type hashed: The hash has to be passed in in text representation.
265 264 """
266 265 password = safe_str(password)
267 266 return crypto_backend().hash_check(password, hashed)
268 267
269 268
270 269 def generate_auth_token(data, salt=None):
271 270 """
272 271 Generates API KEY from given string
273 272 """
274 273
275 274 if salt is None:
276 275 salt = os.urandom(16)
277 276 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 277
279 278
280 279 def get_came_from(request):
281 280 """
282 281 get query_string+path from request sanitized after removing auth_token
283 282 """
284 283 _req = request
285 284
286 285 path = _req.path
287 286 if 'auth_token' in _req.GET:
288 287 # sanitize the request and remove auth_token for redirection
289 288 _req.GET.pop('auth_token')
290 289 qs = _req.query_string
291 290 if qs:
292 291 path += '?' + qs
293 292
294 293 return path
295 294
296 295
297 296 class CookieStoreWrapper(object):
298 297
299 298 def __init__(self, cookie_store):
300 299 self.cookie_store = cookie_store
301 300
302 301 def __repr__(self):
303 302 return 'CookieStore<%s>' % (self.cookie_store)
304 303
305 304 def get(self, key, other=None):
306 305 if isinstance(self.cookie_store, dict):
307 306 return self.cookie_store.get(key, other)
308 307 elif isinstance(self.cookie_store, AuthUser):
309 308 return self.cookie_store.__dict__.get(key, other)
310 309
311 310
312 311 def _cached_perms_data(user_id, scope, user_is_admin,
313 312 user_inherit_default_permissions, explicit, algo,
314 313 calculate_super_admin):
315 314
316 315 permissions = PermissionCalculator(
317 316 user_id, scope, user_is_admin, user_inherit_default_permissions,
318 317 explicit, algo, calculate_super_admin)
319 318 return permissions.calculate()
320 319
321 320
322 321 class PermOrigin(object):
323 322 SUPER_ADMIN = 'superadmin'
324 323 ARCHIVED = 'archived'
325 324
326 325 REPO_USER = 'user:%s'
327 326 REPO_USERGROUP = 'usergroup:%s'
328 327 REPO_OWNER = 'repo.owner'
329 328 REPO_DEFAULT = 'repo.default'
330 329 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
331 330 REPO_PRIVATE = 'repo.private'
332 331
333 332 REPOGROUP_USER = 'user:%s'
334 333 REPOGROUP_USERGROUP = 'usergroup:%s'
335 334 REPOGROUP_OWNER = 'group.owner'
336 335 REPOGROUP_DEFAULT = 'group.default'
337 336 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
338 337
339 338 USERGROUP_USER = 'user:%s'
340 339 USERGROUP_USERGROUP = 'usergroup:%s'
341 340 USERGROUP_OWNER = 'usergroup.owner'
342 341 USERGROUP_DEFAULT = 'usergroup.default'
343 342 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
344 343
345 344
346 345 class PermOriginDict(dict):
347 346 """
348 347 A special dict used for tracking permissions along with their origins.
349 348
350 349 `__setitem__` has been overridden to expect a tuple(perm, origin)
351 350 `__getitem__` will return only the perm
352 351 `.perm_origin_stack` will return the stack of (perm, origin) set per key
353 352
354 353 >>> perms = PermOriginDict()
355 354 >>> perms['resource'] = 'read', 'default', 1
356 355 >>> perms['resource']
357 356 'read'
358 357 >>> perms['resource'] = 'write', 'admin', 2
359 358 >>> perms['resource']
360 359 'write'
361 360 >>> perms.perm_origin_stack
362 361 {'resource': [('read', 'default', 1), ('write', 'admin', 2)]}
363 362 """
364 363
365 364 def __init__(self, *args, **kw):
366 365 dict.__init__(self, *args, **kw)
367 366 self.perm_origin_stack = collections.OrderedDict()
368 367
369 368 def __setitem__(self, key, (perm, origin, obj_id)):
370 369 self.perm_origin_stack.setdefault(key, []).append(
371 370 (perm, origin, obj_id))
372 371 dict.__setitem__(self, key, perm)
373 372
374 373
375 374 class BranchPermOriginDict(PermOriginDict):
376 375 """
377 376 Dedicated branch permissions dict, with tracking of patterns and origins.
378 377
379 378 >>> perms = BranchPermOriginDict()
380 379 >>> perms['resource'] = '*pattern', 'read', 'default'
381 380 >>> perms['resource']
382 381 {'*pattern': 'read'}
383 382 >>> perms['resource'] = '*pattern', 'write', 'admin'
384 383 >>> perms['resource']
385 384 {'*pattern': 'write'}
386 385 >>> perms.perm_origin_stack
387 386 {'resource': {'*pattern': [('read', 'default'), ('write', 'admin')]}}
388 387 """
389 388 def __setitem__(self, key, (pattern, perm, origin)):
390 389
391 390 self.perm_origin_stack.setdefault(key, {}) \
392 391 .setdefault(pattern, []).append((perm, origin))
393 392
394 393 if key in self:
395 394 self[key].__setitem__(pattern, perm)
396 395 else:
397 396 patterns = collections.OrderedDict()
398 397 patterns[pattern] = perm
399 398 dict.__setitem__(self, key, patterns)
400 399
401 400
402 401 class PermissionCalculator(object):
403 402
404 403 def __init__(
405 404 self, user_id, scope, user_is_admin,
406 405 user_inherit_default_permissions, explicit, algo,
407 406 calculate_super_admin_as_user=False):
408 407
409 408 self.user_id = user_id
410 409 self.user_is_admin = user_is_admin
411 410 self.inherit_default_permissions = user_inherit_default_permissions
412 411 self.explicit = explicit
413 412 self.algo = algo
414 413 self.calculate_super_admin_as_user = calculate_super_admin_as_user
415 414
416 415 scope = scope or {}
417 416 self.scope_repo_id = scope.get('repo_id')
418 417 self.scope_repo_group_id = scope.get('repo_group_id')
419 418 self.scope_user_group_id = scope.get('user_group_id')
420 419
421 420 self.default_user_id = User.get_default_user(cache=True).user_id
422 421
423 422 self.permissions_repositories = PermOriginDict()
424 423 self.permissions_repository_groups = PermOriginDict()
425 424 self.permissions_user_groups = PermOriginDict()
426 425 self.permissions_repository_branches = BranchPermOriginDict()
427 426 self.permissions_global = set()
428 427
429 428 self.default_repo_perms = Permission.get_default_repo_perms(
430 429 self.default_user_id, self.scope_repo_id)
431 430 self.default_repo_groups_perms = Permission.get_default_group_perms(
432 431 self.default_user_id, self.scope_repo_group_id)
433 432 self.default_user_group_perms = \
434 433 Permission.get_default_user_group_perms(
435 434 self.default_user_id, self.scope_user_group_id)
436 435
437 436 # default branch perms
438 437 self.default_branch_repo_perms = \
439 438 Permission.get_default_repo_branch_perms(
440 439 self.default_user_id, self.scope_repo_id)
441 440
442 441 def calculate(self):
443 442 if self.user_is_admin and not self.calculate_super_admin_as_user:
444 443 return self._calculate_admin_permissions()
445 444
446 445 self._calculate_global_default_permissions()
447 446 self._calculate_global_permissions()
448 447 self._calculate_default_permissions()
449 448 self._calculate_repository_permissions()
450 449 self._calculate_repository_branch_permissions()
451 450 self._calculate_repository_group_permissions()
452 451 self._calculate_user_group_permissions()
453 452 return self._permission_structure()
454 453
455 454 def _calculate_admin_permissions(self):
456 455 """
457 456 admin user have all default rights for repositories
458 457 and groups set to admin
459 458 """
460 459 self.permissions_global.add('hg.admin')
461 460 self.permissions_global.add('hg.create.write_on_repogroup.true')
462 461
463 462 # repositories
464 463 for perm in self.default_repo_perms:
465 464 r_k = perm.UserRepoToPerm.repository.repo_name
466 465 obj_id = perm.UserRepoToPerm.repository.repo_id
467 466 archived = perm.UserRepoToPerm.repository.archived
468 467 p = 'repository.admin'
469 468 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN, obj_id
470 469 # special case for archived repositories, which we block still even for
471 470 # super admins
472 471 if archived:
473 472 p = 'repository.read'
474 473 self.permissions_repositories[r_k] = p, PermOrigin.ARCHIVED, obj_id
475 474
476 475 # repository groups
477 476 for perm in self.default_repo_groups_perms:
478 477 rg_k = perm.UserRepoGroupToPerm.group.group_name
479 478 obj_id = perm.UserRepoGroupToPerm.group.group_id
480 479 p = 'group.admin'
481 480 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN, obj_id
482 481
483 482 # user groups
484 483 for perm in self.default_user_group_perms:
485 484 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
486 485 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
487 486 p = 'usergroup.admin'
488 487 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN, obj_id
489 488
490 489 # branch permissions
491 490 # since super-admin also can have custom rule permissions
492 491 # we *always* need to calculate those inherited from default, and also explicit
493 492 self._calculate_default_permissions_repository_branches(
494 493 user_inherit_object_permissions=False)
495 494 self._calculate_repository_branch_permissions()
496 495
497 496 return self._permission_structure()
498 497
499 498 def _calculate_global_default_permissions(self):
500 499 """
501 500 global permissions taken from the default user
502 501 """
503 502 default_global_perms = UserToPerm.query()\
504 503 .filter(UserToPerm.user_id == self.default_user_id)\
505 504 .options(joinedload(UserToPerm.permission))
506 505
507 506 for perm in default_global_perms:
508 507 self.permissions_global.add(perm.permission.permission_name)
509 508
510 509 if self.user_is_admin:
511 510 self.permissions_global.add('hg.admin')
512 511 self.permissions_global.add('hg.create.write_on_repogroup.true')
513 512
514 513 def _calculate_global_permissions(self):
515 514 """
516 515 Set global system permissions with user permissions or permissions
517 516 taken from the user groups of the current user.
518 517
519 518 The permissions include repo creating, repo group creating, forking
520 519 etc.
521 520 """
522 521
523 522 # now we read the defined permissions and overwrite what we have set
524 523 # before those can be configured from groups or users explicitly.
525 524
526 525 # In case we want to extend this list we should make sure
527 526 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
528 527 _configurable = frozenset([
529 528 'hg.fork.none', 'hg.fork.repository',
530 529 'hg.create.none', 'hg.create.repository',
531 530 'hg.usergroup.create.false', 'hg.usergroup.create.true',
532 531 'hg.repogroup.create.false', 'hg.repogroup.create.true',
533 532 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
534 533 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
535 534 ])
536 535
537 536 # USER GROUPS comes first user group global permissions
538 537 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
539 538 .options(joinedload(UserGroupToPerm.permission))\
540 539 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
541 540 UserGroupMember.users_group_id))\
542 541 .filter(UserGroupMember.user_id == self.user_id)\
543 542 .order_by(UserGroupToPerm.users_group_id)\
544 543 .all()
545 544
546 545 # need to group here by groups since user can be in more than
547 546 # one group, so we get all groups
548 547 _explicit_grouped_perms = [
549 548 [x, list(y)] for x, y in
550 549 itertools.groupby(user_perms_from_users_groups,
551 550 lambda _x: _x.users_group)]
552 551
553 552 for gr, perms in _explicit_grouped_perms:
554 553 # since user can be in multiple groups iterate over them and
555 554 # select the lowest permissions first (more explicit)
556 555 # TODO(marcink): do this^^
557 556
558 557 # group doesn't inherit default permissions so we actually set them
559 558 if not gr.inherit_default_permissions:
560 559 # NEED TO IGNORE all previously set configurable permissions
561 560 # and replace them with explicitly set from this user
562 561 # group permissions
563 562 self.permissions_global = self.permissions_global.difference(
564 563 _configurable)
565 564 for perm in perms:
566 565 self.permissions_global.add(perm.permission.permission_name)
567 566
568 567 # user explicit global permissions
569 568 user_perms = Session().query(UserToPerm)\
570 569 .options(joinedload(UserToPerm.permission))\
571 570 .filter(UserToPerm.user_id == self.user_id).all()
572 571
573 572 if not self.inherit_default_permissions:
574 573 # NEED TO IGNORE all configurable permissions and
575 574 # replace them with explicitly set from this user permissions
576 575 self.permissions_global = self.permissions_global.difference(
577 576 _configurable)
578 577 for perm in user_perms:
579 578 self.permissions_global.add(perm.permission.permission_name)
580 579
581 580 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
582 581 for perm in self.default_repo_perms:
583 582 r_k = perm.UserRepoToPerm.repository.repo_name
584 583 obj_id = perm.UserRepoToPerm.repository.repo_id
585 584 archived = perm.UserRepoToPerm.repository.archived
586 585 p = perm.Permission.permission_name
587 586 o = PermOrigin.REPO_DEFAULT
588 587 self.permissions_repositories[r_k] = p, o, obj_id
589 588
590 589 # if we decide this user isn't inheriting permissions from
591 590 # default user we set him to .none so only explicit
592 591 # permissions work
593 592 if not user_inherit_object_permissions:
594 593 p = 'repository.none'
595 594 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
596 595 self.permissions_repositories[r_k] = p, o, obj_id
597 596
598 597 if perm.Repository.private and not (
599 598 perm.Repository.user_id == self.user_id):
600 599 # disable defaults for private repos,
601 600 p = 'repository.none'
602 601 o = PermOrigin.REPO_PRIVATE
603 602 self.permissions_repositories[r_k] = p, o, obj_id
604 603
605 604 elif perm.Repository.user_id == self.user_id:
606 605 # set admin if owner
607 606 p = 'repository.admin'
608 607 o = PermOrigin.REPO_OWNER
609 608 self.permissions_repositories[r_k] = p, o, obj_id
610 609
611 610 if self.user_is_admin:
612 611 p = 'repository.admin'
613 612 o = PermOrigin.SUPER_ADMIN
614 613 self.permissions_repositories[r_k] = p, o, obj_id
615 614
616 615 # finally in case of archived repositories, we downgrade higher
617 616 # permissions to read
618 617 if archived:
619 618 current_perm = self.permissions_repositories[r_k]
620 619 if current_perm in ['repository.write', 'repository.admin']:
621 620 p = 'repository.read'
622 621 o = PermOrigin.ARCHIVED
623 622 self.permissions_repositories[r_k] = p, o, obj_id
624 623
625 624 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
626 625 for perm in self.default_branch_repo_perms:
627 626
628 627 r_k = perm.UserRepoToPerm.repository.repo_name
629 628 p = perm.Permission.permission_name
630 629 pattern = perm.UserToRepoBranchPermission.branch_pattern
631 630 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
632 631
633 632 if not self.explicit:
634 633 cur_perm = self.permissions_repository_branches.get(r_k)
635 634 if cur_perm:
636 635 cur_perm = cur_perm[pattern]
637 636 cur_perm = cur_perm or 'branch.none'
638 637
639 638 p = self._choose_permission(p, cur_perm)
640 639
641 640 # NOTE(marcink): register all pattern/perm instances in this
642 641 # special dict that aggregates entries
643 642 self.permissions_repository_branches[r_k] = pattern, p, o
644 643
645 644 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
646 645 for perm in self.default_repo_groups_perms:
647 646 rg_k = perm.UserRepoGroupToPerm.group.group_name
648 647 obj_id = perm.UserRepoGroupToPerm.group.group_id
649 648 p = perm.Permission.permission_name
650 649 o = PermOrigin.REPOGROUP_DEFAULT
651 650 self.permissions_repository_groups[rg_k] = p, o, obj_id
652 651
653 652 # if we decide this user isn't inheriting permissions from default
654 653 # user we set him to .none so only explicit permissions work
655 654 if not user_inherit_object_permissions:
656 655 p = 'group.none'
657 656 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
658 657 self.permissions_repository_groups[rg_k] = p, o, obj_id
659 658
660 659 if perm.RepoGroup.user_id == self.user_id:
661 660 # set admin if owner
662 661 p = 'group.admin'
663 662 o = PermOrigin.REPOGROUP_OWNER
664 663 self.permissions_repository_groups[rg_k] = p, o, obj_id
665 664
666 665 if self.user_is_admin:
667 666 p = 'group.admin'
668 667 o = PermOrigin.SUPER_ADMIN
669 668 self.permissions_repository_groups[rg_k] = p, o, obj_id
670 669
671 670 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
672 671 for perm in self.default_user_group_perms:
673 672 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
674 673 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
675 674 p = perm.Permission.permission_name
676 675 o = PermOrigin.USERGROUP_DEFAULT
677 676 self.permissions_user_groups[u_k] = p, o, obj_id
678 677
679 678 # if we decide this user isn't inheriting permissions from default
680 679 # user we set him to .none so only explicit permissions work
681 680 if not user_inherit_object_permissions:
682 681 p = 'usergroup.none'
683 682 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
684 683 self.permissions_user_groups[u_k] = p, o, obj_id
685 684
686 685 if perm.UserGroup.user_id == self.user_id:
687 686 # set admin if owner
688 687 p = 'usergroup.admin'
689 688 o = PermOrigin.USERGROUP_OWNER
690 689 self.permissions_user_groups[u_k] = p, o, obj_id
691 690
692 691 if self.user_is_admin:
693 692 p = 'usergroup.admin'
694 693 o = PermOrigin.SUPER_ADMIN
695 694 self.permissions_user_groups[u_k] = p, o, obj_id
696 695
697 696 def _calculate_default_permissions(self):
698 697 """
699 698 Set default user permissions for repositories, repository branches,
700 699 repository groups, user groups taken from the default user.
701 700
702 701 Calculate inheritance of object permissions based on what we have now
703 702 in GLOBAL permissions. We check if .false is in GLOBAL since this is
704 703 explicitly set. Inherit is the opposite of .false being there.
705 704
706 705 .. note::
707 706
708 707 the syntax is little bit odd but what we need to check here is
709 708 the opposite of .false permission being in the list so even for
710 709 inconsistent state when both .true/.false is there
711 710 .false is more important
712 711
713 712 """
714 713 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
715 714 in self.permissions_global)
716 715
717 716 # default permissions inherited from `default` user permissions
718 717 self._calculate_default_permissions_repositories(
719 718 user_inherit_object_permissions)
720 719
721 720 self._calculate_default_permissions_repository_branches(
722 721 user_inherit_object_permissions)
723 722
724 723 self._calculate_default_permissions_repository_groups(
725 724 user_inherit_object_permissions)
726 725
727 726 self._calculate_default_permissions_user_groups(
728 727 user_inherit_object_permissions)
729 728
730 729 def _calculate_repository_permissions(self):
731 730 """
732 731 Repository access permissions for the current user.
733 732
734 733 Check if the user is part of user groups for this repository and
735 734 fill in the permission from it. `_choose_permission` decides of which
736 735 permission should be selected based on selected method.
737 736 """
738 737
739 738 # user group for repositories permissions
740 739 user_repo_perms_from_user_group = Permission\
741 740 .get_default_repo_perms_from_user_group(
742 741 self.user_id, self.scope_repo_id)
743 742
744 743 multiple_counter = collections.defaultdict(int)
745 744 for perm in user_repo_perms_from_user_group:
746 745 r_k = perm.UserGroupRepoToPerm.repository.repo_name
747 746 obj_id = perm.UserGroupRepoToPerm.repository.repo_id
748 747 multiple_counter[r_k] += 1
749 748 p = perm.Permission.permission_name
750 749 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
751 750 .users_group.users_group_name
752 751
753 752 if multiple_counter[r_k] > 1:
754 753 cur_perm = self.permissions_repositories[r_k]
755 754 p = self._choose_permission(p, cur_perm)
756 755
757 756 self.permissions_repositories[r_k] = p, o, obj_id
758 757
759 758 if perm.Repository.user_id == self.user_id:
760 759 # set admin if owner
761 760 p = 'repository.admin'
762 761 o = PermOrigin.REPO_OWNER
763 762 self.permissions_repositories[r_k] = p, o, obj_id
764 763
765 764 if self.user_is_admin:
766 765 p = 'repository.admin'
767 766 o = PermOrigin.SUPER_ADMIN
768 767 self.permissions_repositories[r_k] = p, o, obj_id
769 768
770 769 # user explicit permissions for repositories, overrides any specified
771 770 # by the group permission
772 771 user_repo_perms = Permission.get_default_repo_perms(
773 772 self.user_id, self.scope_repo_id)
774 773 for perm in user_repo_perms:
775 774 r_k = perm.UserRepoToPerm.repository.repo_name
776 775 obj_id = perm.UserRepoToPerm.repository.repo_id
777 776 p = perm.Permission.permission_name
778 777 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
779 778
780 779 if not self.explicit:
781 780 cur_perm = self.permissions_repositories.get(
782 781 r_k, 'repository.none')
783 782 p = self._choose_permission(p, cur_perm)
784 783
785 784 self.permissions_repositories[r_k] = p, o, obj_id
786 785
787 786 if perm.Repository.user_id == self.user_id:
788 787 # set admin if owner
789 788 p = 'repository.admin'
790 789 o = PermOrigin.REPO_OWNER
791 790 self.permissions_repositories[r_k] = p, o, obj_id
792 791
793 792 if self.user_is_admin:
794 793 p = 'repository.admin'
795 794 o = PermOrigin.SUPER_ADMIN
796 795 self.permissions_repositories[r_k] = p, o, obj_id
797 796
798 797 def _calculate_repository_branch_permissions(self):
799 798 # user group for repositories permissions
800 799 user_repo_branch_perms_from_user_group = Permission\
801 800 .get_default_repo_branch_perms_from_user_group(
802 801 self.user_id, self.scope_repo_id)
803 802
804 803 multiple_counter = collections.defaultdict(int)
805 804 for perm in user_repo_branch_perms_from_user_group:
806 805 r_k = perm.UserGroupRepoToPerm.repository.repo_name
807 806 p = perm.Permission.permission_name
808 807 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
809 808 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
810 809 .users_group.users_group_name
811 810
812 811 multiple_counter[r_k] += 1
813 812 if multiple_counter[r_k] > 1:
814 813 cur_perm = self.permissions_repository_branches[r_k][pattern]
815 814 p = self._choose_permission(p, cur_perm)
816 815
817 816 self.permissions_repository_branches[r_k] = pattern, p, o
818 817
819 818 # user explicit branch permissions for repositories, overrides
820 819 # any specified by the group permission
821 820 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
822 821 self.user_id, self.scope_repo_id)
823 822
824 823 for perm in user_repo_branch_perms:
825 824
826 825 r_k = perm.UserRepoToPerm.repository.repo_name
827 826 p = perm.Permission.permission_name
828 827 pattern = perm.UserToRepoBranchPermission.branch_pattern
829 828 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
830 829
831 830 if not self.explicit:
832 831 cur_perm = self.permissions_repository_branches.get(r_k)
833 832 if cur_perm:
834 833 cur_perm = cur_perm[pattern]
835 834 cur_perm = cur_perm or 'branch.none'
836 835 p = self._choose_permission(p, cur_perm)
837 836
838 837 # NOTE(marcink): register all pattern/perm instances in this
839 838 # special dict that aggregates entries
840 839 self.permissions_repository_branches[r_k] = pattern, p, o
841 840
842 841 def _calculate_repository_group_permissions(self):
843 842 """
844 843 Repository group permissions for the current user.
845 844
846 845 Check if the user is part of user groups for repository groups and
847 846 fill in the permissions from it. `_choose_permission` decides of which
848 847 permission should be selected based on selected method.
849 848 """
850 849 # user group for repo groups permissions
851 850 user_repo_group_perms_from_user_group = Permission\
852 851 .get_default_group_perms_from_user_group(
853 852 self.user_id, self.scope_repo_group_id)
854 853
855 854 multiple_counter = collections.defaultdict(int)
856 855 for perm in user_repo_group_perms_from_user_group:
857 856 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
858 857 obj_id = perm.UserGroupRepoGroupToPerm.group.group_id
859 858 multiple_counter[rg_k] += 1
860 859 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
861 860 .users_group.users_group_name
862 861 p = perm.Permission.permission_name
863 862
864 863 if multiple_counter[rg_k] > 1:
865 864 cur_perm = self.permissions_repository_groups[rg_k]
866 865 p = self._choose_permission(p, cur_perm)
867 866 self.permissions_repository_groups[rg_k] = p, o, obj_id
868 867
869 868 if perm.RepoGroup.user_id == self.user_id:
870 869 # set admin if owner, even for member of other user group
871 870 p = 'group.admin'
872 871 o = PermOrigin.REPOGROUP_OWNER
873 872 self.permissions_repository_groups[rg_k] = p, o, obj_id
874 873
875 874 if self.user_is_admin:
876 875 p = 'group.admin'
877 876 o = PermOrigin.SUPER_ADMIN
878 877 self.permissions_repository_groups[rg_k] = p, o, obj_id
879 878
880 879 # user explicit permissions for repository groups
881 880 user_repo_groups_perms = Permission.get_default_group_perms(
882 881 self.user_id, self.scope_repo_group_id)
883 882 for perm in user_repo_groups_perms:
884 883 rg_k = perm.UserRepoGroupToPerm.group.group_name
885 884 obj_id = perm.UserRepoGroupToPerm.group.group_id
886 885 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
887 886 .user.username
888 887 p = perm.Permission.permission_name
889 888
890 889 if not self.explicit:
891 890 cur_perm = self.permissions_repository_groups.get(rg_k, 'group.none')
892 891 p = self._choose_permission(p, cur_perm)
893 892
894 893 self.permissions_repository_groups[rg_k] = p, o, obj_id
895 894
896 895 if perm.RepoGroup.user_id == self.user_id:
897 896 # set admin if owner
898 897 p = 'group.admin'
899 898 o = PermOrigin.REPOGROUP_OWNER
900 899 self.permissions_repository_groups[rg_k] = p, o, obj_id
901 900
902 901 if self.user_is_admin:
903 902 p = 'group.admin'
904 903 o = PermOrigin.SUPER_ADMIN
905 904 self.permissions_repository_groups[rg_k] = p, o, obj_id
906 905
907 906 def _calculate_user_group_permissions(self):
908 907 """
909 908 User group permissions for the current user.
910 909 """
911 910 # user group for user group permissions
912 911 user_group_from_user_group = Permission\
913 912 .get_default_user_group_perms_from_user_group(
914 913 self.user_id, self.scope_user_group_id)
915 914
916 915 multiple_counter = collections.defaultdict(int)
917 916 for perm in user_group_from_user_group:
918 917 ug_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
919 918 obj_id = perm.UserGroupUserGroupToPerm.target_user_group.users_group_id
920 919 multiple_counter[ug_k] += 1
921 920 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
922 921 .user_group.users_group_name
923 922 p = perm.Permission.permission_name
924 923
925 924 if multiple_counter[ug_k] > 1:
926 925 cur_perm = self.permissions_user_groups[ug_k]
927 926 p = self._choose_permission(p, cur_perm)
928 927
929 928 self.permissions_user_groups[ug_k] = p, o, obj_id
930 929
931 930 if perm.UserGroup.user_id == self.user_id:
932 931 # set admin if owner, even for member of other user group
933 932 p = 'usergroup.admin'
934 933 o = PermOrigin.USERGROUP_OWNER
935 934 self.permissions_user_groups[ug_k] = p, o, obj_id
936 935
937 936 if self.user_is_admin:
938 937 p = 'usergroup.admin'
939 938 o = PermOrigin.SUPER_ADMIN
940 939 self.permissions_user_groups[ug_k] = p, o, obj_id
941 940
942 941 # user explicit permission for user groups
943 942 user_user_groups_perms = Permission.get_default_user_group_perms(
944 943 self.user_id, self.scope_user_group_id)
945 944 for perm in user_user_groups_perms:
946 945 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
947 946 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
948 947 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
949 948 .user.username
950 949 p = perm.Permission.permission_name
951 950
952 951 if not self.explicit:
953 952 cur_perm = self.permissions_user_groups.get(ug_k, 'usergroup.none')
954 953 p = self._choose_permission(p, cur_perm)
955 954
956 955 self.permissions_user_groups[ug_k] = p, o, obj_id
957 956
958 957 if perm.UserGroup.user_id == self.user_id:
959 958 # set admin if owner
960 959 p = 'usergroup.admin'
961 960 o = PermOrigin.USERGROUP_OWNER
962 961 self.permissions_user_groups[ug_k] = p, o, obj_id
963 962
964 963 if self.user_is_admin:
965 964 p = 'usergroup.admin'
966 965 o = PermOrigin.SUPER_ADMIN
967 966 self.permissions_user_groups[ug_k] = p, o, obj_id
968 967
969 968 def _choose_permission(self, new_perm, cur_perm):
970 969 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
971 970 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
972 971 if self.algo == 'higherwin':
973 972 if new_perm_val > cur_perm_val:
974 973 return new_perm
975 974 return cur_perm
976 975 elif self.algo == 'lowerwin':
977 976 if new_perm_val < cur_perm_val:
978 977 return new_perm
979 978 return cur_perm
980 979
981 980 def _permission_structure(self):
982 981 return {
983 982 'global': self.permissions_global,
984 983 'repositories': self.permissions_repositories,
985 984 'repository_branches': self.permissions_repository_branches,
986 985 'repositories_groups': self.permissions_repository_groups,
987 986 'user_groups': self.permissions_user_groups,
988 987 }
989 988
990 989
991 990 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
992 991 """
993 992 Check if given controller_name is in whitelist of auth token access
994 993 """
995 994 if not whitelist:
996 995 from rhodecode import CONFIG
997 996 whitelist = aslist(
998 997 CONFIG.get('api_access_controllers_whitelist'), sep=',')
999 998 # backward compat translation
1000 999 compat = {
1001 1000 # old controller, new VIEW
1002 1001 'ChangesetController:*': 'RepoCommitsView:*',
1003 1002 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
1004 1003 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
1005 1004 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
1006 1005 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
1007 1006 'GistsController:*': 'GistView:*',
1008 1007 }
1009 1008
1010 1009 log.debug(
1011 1010 'Allowed views for AUTH TOKEN access: %s', whitelist)
1012 1011 auth_token_access_valid = False
1013 1012
1014 1013 for entry in whitelist:
1015 1014 token_match = True
1016 1015 if entry in compat:
1017 1016 # translate from old Controllers to Pyramid Views
1018 1017 entry = compat[entry]
1019 1018
1020 1019 if '@' in entry:
1021 1020 # specific AuthToken
1022 1021 entry, allowed_token = entry.split('@', 1)
1023 1022 token_match = auth_token == allowed_token
1024 1023
1025 1024 if fnmatch.fnmatch(view_name, entry) and token_match:
1026 1025 auth_token_access_valid = True
1027 1026 break
1028 1027
1029 1028 if auth_token_access_valid:
1030 1029 log.debug('view: `%s` matches entry in whitelist: %s',
1031 1030 view_name, whitelist)
1032 1031
1033 1032 else:
1034 1033 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
1035 1034 % (view_name, whitelist))
1036 1035 if auth_token:
1037 1036 # if we use auth token key and don't have access it's a warning
1038 1037 log.warning(msg)
1039 1038 else:
1040 1039 log.debug(msg)
1041 1040
1042 1041 return auth_token_access_valid
1043 1042
1044 1043
1045 1044 class AuthUser(object):
1046 1045 """
1047 1046 A simple object that handles all attributes of user in RhodeCode
1048 1047
1049 1048 It does lookup based on API key,given user, or user present in session
1050 1049 Then it fills all required information for such user. It also checks if
1051 1050 anonymous access is enabled and if so, it returns default user as logged in
1052 1051 """
1053 1052 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1054 1053 repo_read_perms = ['repository.read', 'repository.admin', 'repository.write']
1055 1054 repo_group_read_perms = ['group.read', 'group.write', 'group.admin']
1056 1055 user_group_read_perms = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
1057 1056
1058 1057 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1059 1058
1060 1059 self.user_id = user_id
1061 1060 self._api_key = api_key
1062 1061
1063 1062 self.api_key = None
1064 1063 self.username = username
1065 1064 self.ip_addr = ip_addr
1066 1065 self.name = ''
1067 1066 self.lastname = ''
1068 1067 self.first_name = ''
1069 1068 self.last_name = ''
1070 1069 self.email = ''
1071 1070 self.is_authenticated = False
1072 1071 self.admin = False
1073 1072 self.inherit_default_permissions = False
1074 1073 self.password = ''
1075 1074
1076 1075 self.anonymous_user = None # propagated on propagate_data
1077 1076 self.propagate_data()
1078 1077 self._instance = None
1079 1078 self._permissions_scoped_cache = {} # used to bind scoped calculation
1080 1079
1081 1080 @LazyProperty
1082 1081 def permissions(self):
1083 1082 return self.get_perms(user=self, cache=None)
1084 1083
1085 1084 @LazyProperty
1086 1085 def permissions_safe(self):
1087 1086 """
1088 1087 Filtered permissions excluding not allowed repositories
1089 1088 """
1090 1089 perms = self.get_perms(user=self, cache=None)
1091 1090
1092 1091 perms['repositories'] = {
1093 1092 k: v for k, v in perms['repositories'].items()
1094 1093 if v != 'repository.none'}
1095 1094 perms['repositories_groups'] = {
1096 1095 k: v for k, v in perms['repositories_groups'].items()
1097 1096 if v != 'group.none'}
1098 1097 perms['user_groups'] = {
1099 1098 k: v for k, v in perms['user_groups'].items()
1100 1099 if v != 'usergroup.none'}
1101 1100 perms['repository_branches'] = {
1102 1101 k: v for k, v in perms['repository_branches'].iteritems()
1103 1102 if v != 'branch.none'}
1104 1103 return perms
1105 1104
1106 1105 @LazyProperty
1107 1106 def permissions_full_details(self):
1108 1107 return self.get_perms(
1109 1108 user=self, cache=None, calculate_super_admin=True)
1110 1109
1111 1110 def permissions_with_scope(self, scope):
1112 1111 """
1113 1112 Call the get_perms function with scoped data. The scope in that function
1114 1113 narrows the SQL calls to the given ID of objects resulting in fetching
1115 1114 Just particular permission we want to obtain. If scope is an empty dict
1116 1115 then it basically narrows the scope to GLOBAL permissions only.
1117 1116
1118 1117 :param scope: dict
1119 1118 """
1120 1119 if 'repo_name' in scope:
1121 1120 obj = Repository.get_by_repo_name(scope['repo_name'])
1122 1121 if obj:
1123 1122 scope['repo_id'] = obj.repo_id
1124 1123 _scope = collections.OrderedDict()
1125 1124 _scope['repo_id'] = -1
1126 1125 _scope['user_group_id'] = -1
1127 1126 _scope['repo_group_id'] = -1
1128 1127
1129 1128 for k in sorted(scope.keys()):
1130 1129 _scope[k] = scope[k]
1131 1130
1132 1131 # store in cache to mimic how the @LazyProperty works,
1133 1132 # the difference here is that we use the unique key calculated
1134 1133 # from params and values
1135 1134 return self.get_perms(user=self, cache=None, scope=_scope)
1136 1135
1137 1136 def get_instance(self):
1138 1137 return User.get(self.user_id)
1139 1138
1140 1139 def propagate_data(self):
1141 1140 """
1142 1141 Fills in user data and propagates values to this instance. Maps fetched
1143 1142 user attributes to this class instance attributes
1144 1143 """
1145 1144 log.debug('AuthUser: starting data propagation for new potential user')
1146 1145 user_model = UserModel()
1147 1146 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1148 1147 is_user_loaded = False
1149 1148
1150 1149 # lookup by userid
1151 1150 if self.user_id is not None and self.user_id != anon_user.user_id:
1152 1151 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1153 1152 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1154 1153
1155 1154 # try go get user by api key
1156 1155 elif self._api_key and self._api_key != anon_user.api_key:
1157 1156 log.debug('Trying Auth User lookup by API KEY: `...%s`', self._api_key[-4:])
1158 1157 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1159 1158
1160 1159 # lookup by username
1161 1160 elif self.username:
1162 1161 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1163 1162 is_user_loaded = user_model.fill_data(self, username=self.username)
1164 1163 else:
1165 1164 log.debug('No data in %s that could been used to log in', self)
1166 1165
1167 1166 if not is_user_loaded:
1168 1167 log.debug(
1169 1168 'Failed to load user. Fallback to default user %s', anon_user)
1170 1169 # if we cannot authenticate user try anonymous
1171 1170 if anon_user.active:
1172 1171 log.debug('default user is active, using it as a session user')
1173 1172 user_model.fill_data(self, user_id=anon_user.user_id)
1174 1173 # then we set this user is logged in
1175 1174 self.is_authenticated = True
1176 1175 else:
1177 1176 log.debug('default user is NOT active')
1178 1177 # in case of disabled anonymous user we reset some of the
1179 1178 # parameters so such user is "corrupted", skipping the fill_data
1180 1179 for attr in ['user_id', 'username', 'admin', 'active']:
1181 1180 setattr(self, attr, None)
1182 1181 self.is_authenticated = False
1183 1182
1184 1183 if not self.username:
1185 1184 self.username = 'None'
1186 1185
1187 1186 log.debug('AuthUser: propagated user is now %s', self)
1188 1187
1189 1188 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1190 1189 calculate_super_admin=False, cache=None):
1191 1190 """
1192 1191 Fills user permission attribute with permissions taken from database
1193 1192 works for permissions given for repositories, and for permissions that
1194 1193 are granted to groups
1195 1194
1196 1195 :param user: instance of User object from database
1197 1196 :param explicit: In case there are permissions both for user and a group
1198 1197 that user is part of, explicit flag will defiine if user will
1199 1198 explicitly override permissions from group, if it's False it will
1200 1199 make decision based on the algo
1201 1200 :param algo: algorithm to decide what permission should be choose if
1202 1201 it's multiple defined, eg user in two different groups. It also
1203 1202 decides if explicit flag is turned off how to specify the permission
1204 1203 for case when user is in a group + have defined separate permission
1205 1204 :param calculate_super_admin: calculate permissions for super-admin in the
1206 1205 same way as for regular user without speedups
1207 1206 :param cache: Use caching for calculation, None = let the cache backend decide
1208 1207 """
1209 1208 user_id = user.user_id
1210 1209 user_is_admin = user.is_admin
1211 1210
1212 1211 # inheritance of global permissions like create repo/fork repo etc
1213 1212 user_inherit_default_permissions = user.inherit_default_permissions
1214 1213
1215 1214 cache_seconds = safe_int(
1216 1215 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1217 1216
1218 1217 if cache is None:
1219 1218 # let the backend cache decide
1220 1219 cache_on = cache_seconds > 0
1221 1220 else:
1222 1221 cache_on = cache
1223 1222
1224 1223 log.debug(
1225 1224 'Computing PERMISSION tree for user %s scope `%s` '
1226 1225 'with caching: %s[TTL: %ss]', user, scope, cache_on, cache_seconds or 0)
1227 1226
1228 1227 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1229 1228 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1230 1229
1231 1230 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1232 1231 condition=cache_on)
1233 1232 def compute_perm_tree(cache_name, cache_ver,
1234 1233 user_id, scope, user_is_admin,user_inherit_default_permissions,
1235 1234 explicit, algo, calculate_super_admin):
1236 1235 return _cached_perms_data(
1237 1236 user_id, scope, user_is_admin, user_inherit_default_permissions,
1238 1237 explicit, algo, calculate_super_admin)
1239 1238
1240 1239 start = time.time()
1241 1240 result = compute_perm_tree(
1242 1241 'permissions', 'v1', user_id, scope, user_is_admin,
1243 1242 user_inherit_default_permissions, explicit, algo,
1244 1243 calculate_super_admin)
1245 1244
1246 1245 result_repr = []
1247 1246 for k in result:
1248 1247 result_repr.append((k, len(result[k])))
1249 1248 total = time.time() - start
1250 1249 log.debug('PERMISSION tree for user %s computed in %.4fs: %s',
1251 1250 user, total, result_repr)
1252 1251
1253 1252 return result
1254 1253
1255 1254 @property
1256 1255 def is_default(self):
1257 1256 return self.username == User.DEFAULT_USER
1258 1257
1259 1258 @property
1260 1259 def is_admin(self):
1261 1260 return self.admin
1262 1261
1263 1262 @property
1264 1263 def is_user_object(self):
1265 1264 return self.user_id is not None
1266 1265
1267 1266 @property
1268 1267 def repositories_admin(self):
1269 1268 """
1270 1269 Returns list of repositories you're an admin of
1271 1270 """
1272 1271 return [
1273 1272 x[0] for x in self.permissions['repositories'].items()
1274 1273 if x[1] == 'repository.admin']
1275 1274
1276 1275 @property
1277 1276 def repository_groups_admin(self):
1278 1277 """
1279 1278 Returns list of repository groups you're an admin of
1280 1279 """
1281 1280 return [
1282 1281 x[0] for x in self.permissions['repositories_groups'].items()
1283 1282 if x[1] == 'group.admin']
1284 1283
1285 1284 @property
1286 1285 def user_groups_admin(self):
1287 1286 """
1288 1287 Returns list of user groups you're an admin of
1289 1288 """
1290 1289 return [
1291 1290 x[0] for x in self.permissions['user_groups'].items()
1292 1291 if x[1] == 'usergroup.admin']
1293 1292
1294 1293 def repo_acl_ids_from_stack(self, perms=None, prefix_filter=None, cache=False):
1295 1294 if not perms:
1296 1295 perms = AuthUser.repo_read_perms
1297 1296 allowed_ids = []
1298 1297 for k, stack_data in self.permissions['repositories'].perm_origin_stack.items():
1299 1298 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1300 1299 if prefix_filter and not k.startswith(prefix_filter):
1301 1300 continue
1302 1301 if perm in perms:
1303 1302 allowed_ids.append(obj_id)
1304 1303 return allowed_ids
1305 1304
1306 1305 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1307 1306 """
1308 1307 Returns list of repository ids that user have access to based on given
1309 1308 perms. The cache flag should be only used in cases that are used for
1310 1309 display purposes, NOT IN ANY CASE for permission checks.
1311 1310 """
1312 1311 from rhodecode.model.scm import RepoList
1313 1312 if not perms:
1314 1313 perms = AuthUser.repo_read_perms
1315 1314
1316 1315 def _cached_repo_acl(user_id, perm_def, _name_filter):
1317 1316 qry = Repository.query()
1318 1317 if _name_filter:
1319 1318 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1320 1319 qry = qry.filter(
1321 1320 Repository.repo_name.ilike(ilike_expression))
1322 1321
1323 1322 return [x.repo_id for x in
1324 1323 RepoList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1325 1324
1326 1325 return _cached_repo_acl(self.user_id, perms, name_filter)
1327 1326
1328 1327 def repo_group_acl_ids_from_stack(self, perms=None, prefix_filter=None, cache=False):
1329 1328 if not perms:
1330 1329 perms = AuthUser.repo_group_read_perms
1331 1330 allowed_ids = []
1332 1331 for k, stack_data in self.permissions['repositories_groups'].perm_origin_stack.items():
1333 1332 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1334 1333 if prefix_filter and not k.startswith(prefix_filter):
1335 1334 continue
1336 1335 if perm in perms:
1337 1336 allowed_ids.append(obj_id)
1338 1337 return allowed_ids
1339 1338
1340 1339 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1341 1340 """
1342 1341 Returns list of repository group ids that user have access to based on given
1343 1342 perms. The cache flag should be only used in cases that are used for
1344 1343 display purposes, NOT IN ANY CASE for permission checks.
1345 1344 """
1346 1345 from rhodecode.model.scm import RepoGroupList
1347 1346 if not perms:
1348 1347 perms = AuthUser.repo_group_read_perms
1349 1348
1350 1349 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1351 1350 qry = RepoGroup.query()
1352 1351 if _name_filter:
1353 1352 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1354 1353 qry = qry.filter(
1355 1354 RepoGroup.group_name.ilike(ilike_expression))
1356 1355
1357 1356 return [x.group_id for x in
1358 1357 RepoGroupList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1359 1358
1360 1359 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1361 1360
1362 1361 def user_group_acl_ids_from_stack(self, perms=None, cache=False):
1363 1362 if not perms:
1364 1363 perms = AuthUser.user_group_read_perms
1365 1364 allowed_ids = []
1366 1365 for k, stack_data in self.permissions['user_groups'].perm_origin_stack.items():
1367 1366 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1368 1367 if perm in perms:
1369 1368 allowed_ids.append(obj_id)
1370 1369 return allowed_ids
1371 1370
1372 1371 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1373 1372 """
1374 1373 Returns list of user group ids that user have access to based on given
1375 1374 perms. The cache flag should be only used in cases that are used for
1376 1375 display purposes, NOT IN ANY CASE for permission checks.
1377 1376 """
1378 1377 from rhodecode.model.scm import UserGroupList
1379 1378 if not perms:
1380 1379 perms = AuthUser.user_group_read_perms
1381 1380
1382 1381 def _cached_user_group_acl(user_id, perm_def, name_filter):
1383 1382 qry = UserGroup.query()
1384 1383 if name_filter:
1385 1384 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1386 1385 qry = qry.filter(
1387 1386 UserGroup.users_group_name.ilike(ilike_expression))
1388 1387
1389 1388 return [x.users_group_id for x in
1390 1389 UserGroupList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1391 1390
1392 1391 return _cached_user_group_acl(self.user_id, perms, name_filter)
1393 1392
1394 1393 @property
1395 1394 def ip_allowed(self):
1396 1395 """
1397 1396 Checks if ip_addr used in constructor is allowed from defined list of
1398 1397 allowed ip_addresses for user
1399 1398
1400 1399 :returns: boolean, True if ip is in allowed ip range
1401 1400 """
1402 1401 # check IP
1403 1402 inherit = self.inherit_default_permissions
1404 1403 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1405 1404 inherit_from_default=inherit)
1406 1405 @property
1407 1406 def personal_repo_group(self):
1408 1407 return RepoGroup.get_user_personal_repo_group(self.user_id)
1409 1408
1410 1409 @LazyProperty
1411 1410 def feed_token(self):
1412 1411 return self.get_instance().feed_token
1413 1412
1414 1413 @LazyProperty
1415 1414 def artifact_token(self):
1416 1415 return self.get_instance().artifact_token
1417 1416
1418 1417 @classmethod
1419 1418 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1420 1419 allowed_ips = AuthUser.get_allowed_ips(
1421 1420 user_id, cache=True, inherit_from_default=inherit_from_default)
1422 1421 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1423 1422 log.debug('IP:%s for user %s is in range of %s',
1424 1423 ip_addr, user_id, allowed_ips)
1425 1424 return True
1426 1425 else:
1427 1426 log.info('Access for IP:%s forbidden for user %s, '
1428 1427 'not in %s', ip_addr, user_id, allowed_ips)
1429 1428 return False
1430 1429
1431 1430 def get_branch_permissions(self, repo_name, perms=None):
1432 1431 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1433 1432 branch_perms = perms.get('repository_branches', {})
1434 1433 if not branch_perms:
1435 1434 return {}
1436 1435 repo_branch_perms = branch_perms.get(repo_name)
1437 1436 return repo_branch_perms or {}
1438 1437
1439 1438 def get_rule_and_branch_permission(self, repo_name, branch_name):
1440 1439 """
1441 1440 Check if this AuthUser has defined any permissions for branches. If any of
1442 1441 the rules match in order, we return the matching permissions
1443 1442 """
1444 1443
1445 1444 rule = default_perm = ''
1446 1445
1447 1446 repo_branch_perms = self.get_branch_permissions(repo_name=repo_name)
1448 1447 if not repo_branch_perms:
1449 1448 return rule, default_perm
1450 1449
1451 1450 # now calculate the permissions
1452 1451 for pattern, branch_perm in repo_branch_perms.items():
1453 1452 if fnmatch.fnmatch(branch_name, pattern):
1454 1453 rule = '`{}`=>{}'.format(pattern, branch_perm)
1455 1454 return rule, branch_perm
1456 1455
1457 1456 return rule, default_perm
1458 1457
1459 1458 def __repr__(self):
1460 1459 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1461 1460 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1462 1461
1463 1462 def set_authenticated(self, authenticated=True):
1464 1463 if self.user_id != self.anonymous_user.user_id:
1465 1464 self.is_authenticated = authenticated
1466 1465
1467 1466 def get_cookie_store(self):
1468 1467 return {
1469 1468 'username': self.username,
1470 1469 'password': md5(self.password or ''),
1471 1470 'user_id': self.user_id,
1472 1471 'is_authenticated': self.is_authenticated
1473 1472 }
1474 1473
1475 1474 @classmethod
1476 1475 def from_cookie_store(cls, cookie_store):
1477 1476 """
1478 1477 Creates AuthUser from a cookie store
1479 1478
1480 1479 :param cls:
1481 1480 :param cookie_store:
1482 1481 """
1483 1482 user_id = cookie_store.get('user_id')
1484 1483 username = cookie_store.get('username')
1485 1484 api_key = cookie_store.get('api_key')
1486 1485 return AuthUser(user_id, api_key, username)
1487 1486
1488 1487 @classmethod
1489 1488 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1490 1489 _set = set()
1491 1490
1492 1491 if inherit_from_default:
1493 1492 def_user_id = User.get_default_user(cache=True).user_id
1494 1493 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1495 1494 if cache:
1496 1495 default_ips = default_ips.options(
1497 1496 FromCache("sql_cache_short", "get_user_ips_default"))
1498 1497
1499 1498 # populate from default user
1500 1499 for ip in default_ips:
1501 1500 try:
1502 1501 _set.add(ip.ip_addr)
1503 1502 except ObjectDeletedError:
1504 1503 # since we use heavy caching sometimes it happens that
1505 1504 # we get deleted objects here, we just skip them
1506 1505 pass
1507 1506
1508 1507 # NOTE:(marcink) we don't want to load any rules for empty
1509 1508 # user_id which is the case of access of non logged users when anonymous
1510 1509 # access is disabled
1511 1510 user_ips = []
1512 1511 if user_id:
1513 1512 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1514 1513 if cache:
1515 1514 user_ips = user_ips.options(
1516 1515 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1517 1516
1518 1517 for ip in user_ips:
1519 1518 try:
1520 1519 _set.add(ip.ip_addr)
1521 1520 except ObjectDeletedError:
1522 1521 # since we use heavy caching sometimes it happens that we get
1523 1522 # deleted objects here, we just skip them
1524 1523 pass
1525 1524 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1526 1525
1527 1526
1528 1527 def set_available_permissions(settings):
1529 1528 """
1530 1529 This function will propagate pyramid settings with all available defined
1531 1530 permission given in db. We don't want to check each time from db for new
1532 1531 permissions since adding a new permission also requires application restart
1533 1532 ie. to decorate new views with the newly created permission
1534 1533
1535 1534 :param settings: current pyramid registry.settings
1536 1535
1537 1536 """
1538 1537 log.debug('auth: getting information about all available permissions')
1539 1538 try:
1540 1539 sa = meta.Session
1541 1540 all_perms = sa.query(Permission).all()
1542 1541 settings.setdefault('available_permissions',
1543 1542 [x.permission_name for x in all_perms])
1544 1543 log.debug('auth: set available permissions')
1545 1544 except Exception:
1546 1545 log.exception('Failed to fetch permissions from the database.')
1547 1546 raise
1548 1547
1549 1548
1550 1549 def get_csrf_token(session, force_new=False, save_if_missing=True):
1551 1550 """
1552 1551 Return the current authentication token, creating one if one doesn't
1553 1552 already exist and the save_if_missing flag is present.
1554 1553
1555 1554 :param session: pass in the pyramid session, else we use the global ones
1556 1555 :param force_new: force to re-generate the token and store it in session
1557 1556 :param save_if_missing: save the newly generated token if it's missing in
1558 1557 session
1559 1558 """
1560 1559 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1561 1560 # from pyramid.csrf import get_csrf_token
1562 1561
1563 1562 if (csrf_token_key not in session and save_if_missing) or force_new:
1564 1563 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1565 1564 session[csrf_token_key] = token
1566 1565 if hasattr(session, 'save'):
1567 1566 session.save()
1568 1567 return session.get(csrf_token_key)
1569 1568
1570 1569
1571 1570 def get_request(perm_class_instance):
1572 1571 from pyramid.threadlocal import get_current_request
1573 1572 pyramid_request = get_current_request()
1574 1573 return pyramid_request
1575 1574
1576 1575
1577 1576 # CHECK DECORATORS
1578 1577 class CSRFRequired(object):
1579 1578 """
1580 1579 Decorator for authenticating a form
1581 1580
1582 1581 This decorator uses an authorization token stored in the client's
1583 1582 session for prevention of certain Cross-site request forgery (CSRF)
1584 1583 attacks (See
1585 1584 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1586 1585 information).
1587 1586
1588 1587 For use with the ``secure_form`` helper functions.
1589 1588
1590 1589 """
1591 1590 def __init__(self, token=csrf_token_key, header='X-CSRF-Token', except_methods=None):
1592 1591 self.token = token
1593 1592 self.header = header
1594 1593 self.except_methods = except_methods or []
1595 1594
1596 1595 def __call__(self, func):
1597 1596 return get_cython_compat_decorator(self.__wrapper, func)
1598 1597
1599 1598 def _get_csrf(self, _request):
1600 1599 return _request.POST.get(self.token, _request.headers.get(self.header))
1601 1600
1602 1601 def check_csrf(self, _request, cur_token):
1603 1602 supplied_token = self._get_csrf(_request)
1604 1603 return supplied_token and supplied_token == cur_token
1605 1604
1606 1605 def _get_request(self):
1607 1606 return get_request(self)
1608 1607
1609 1608 def __wrapper(self, func, *fargs, **fkwargs):
1610 1609 request = self._get_request()
1611 1610
1612 1611 if request.method in self.except_methods:
1613 1612 return func(*fargs, **fkwargs)
1614 1613
1615 1614 cur_token = get_csrf_token(request.session, save_if_missing=False)
1616 1615 if self.check_csrf(request, cur_token):
1617 1616 if request.POST.get(self.token):
1618 1617 del request.POST[self.token]
1619 1618 return func(*fargs, **fkwargs)
1620 1619 else:
1621 1620 reason = 'token-missing'
1622 1621 supplied_token = self._get_csrf(request)
1623 1622 if supplied_token and cur_token != supplied_token:
1624 1623 reason = 'token-mismatch [%s:%s]' % (
1625 1624 cur_token or ''[:6], supplied_token or ''[:6])
1626 1625
1627 1626 csrf_message = \
1628 1627 ("Cross-site request forgery detected, request denied. See "
1629 1628 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1630 1629 "more information.")
1631 1630 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1632 1631 'REMOTE_ADDR:%s, HEADERS:%s' % (
1633 1632 request, reason, request.remote_addr, request.headers))
1634 1633
1635 1634 raise HTTPForbidden(explanation=csrf_message)
1636 1635
1637 1636
1638 1637 class LoginRequired(object):
1639 1638 """
1640 1639 Must be logged in to execute this function else
1641 1640 redirect to login page
1642 1641
1643 1642 :param api_access: if enabled this checks only for valid auth token
1644 1643 and grants access based on valid token
1645 1644 """
1646 1645 def __init__(self, auth_token_access=None):
1647 1646 self.auth_token_access = auth_token_access
1648 1647 if self.auth_token_access:
1649 1648 valid_type = set(auth_token_access).intersection(set(UserApiKeys.ROLES))
1650 1649 if not valid_type:
1651 1650 raise ValueError('auth_token_access must be on of {}, got {}'.format(
1652 1651 UserApiKeys.ROLES, auth_token_access))
1653 1652
1654 1653 def __call__(self, func):
1655 1654 return get_cython_compat_decorator(self.__wrapper, func)
1656 1655
1657 1656 def _get_request(self):
1658 1657 return get_request(self)
1659 1658
1660 1659 def __wrapper(self, func, *fargs, **fkwargs):
1661 1660 from rhodecode.lib import helpers as h
1662 1661 cls = fargs[0]
1663 1662 user = cls._rhodecode_user
1664 1663 request = self._get_request()
1665 1664 _ = request.translate
1666 1665
1667 1666 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1668 1667 log.debug('Starting login restriction checks for user: %s', user)
1669 1668 # check if our IP is allowed
1670 1669 ip_access_valid = True
1671 1670 if not user.ip_allowed:
1672 1671 h.flash(h.literal(_('IP {} not allowed'.format(user.ip_addr))),
1673 1672 category='warning')
1674 1673 ip_access_valid = False
1675 1674
1676 1675 # we used stored token that is extract from GET or URL param (if any)
1677 1676 _auth_token = request.user_auth_token
1678 1677
1679 1678 # check if we used an AUTH_TOKEN and it's a valid one
1680 1679 # defined white-list of controllers which API access will be enabled
1681 1680 whitelist = None
1682 1681 if self.auth_token_access:
1683 1682 # since this location is allowed by @LoginRequired decorator it's our
1684 1683 # only whitelist
1685 1684 whitelist = [loc]
1686 1685 auth_token_access_valid = allowed_auth_token_access(
1687 1686 loc, whitelist=whitelist, auth_token=_auth_token)
1688 1687
1689 1688 # explicit controller is enabled or API is in our whitelist
1690 1689 if auth_token_access_valid:
1691 1690 log.debug('Checking AUTH TOKEN access for %s', cls)
1692 1691 db_user = user.get_instance()
1693 1692
1694 1693 if db_user:
1695 1694 if self.auth_token_access:
1696 1695 roles = self.auth_token_access
1697 1696 else:
1698 1697 roles = [UserApiKeys.ROLE_HTTP]
1699 1698 log.debug('AUTH TOKEN: checking auth for user %s and roles %s',
1700 1699 db_user, roles)
1701 1700 token_match = db_user.authenticate_by_token(
1702 1701 _auth_token, roles=roles)
1703 1702 else:
1704 1703 log.debug('Unable to fetch db instance for auth user: %s', user)
1705 1704 token_match = False
1706 1705
1707 1706 if _auth_token and token_match:
1708 1707 auth_token_access_valid = True
1709 1708 log.debug('AUTH TOKEN ****%s is VALID', _auth_token[-4:])
1710 1709 else:
1711 1710 auth_token_access_valid = False
1712 1711 if not _auth_token:
1713 1712 log.debug("AUTH TOKEN *NOT* present in request")
1714 1713 else:
1715 1714 log.warning("AUTH TOKEN ****%s *NOT* valid", _auth_token[-4:])
1716 1715
1717 1716 log.debug('Checking if %s is authenticated @ %s', user.username, loc)
1718 1717 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1719 1718 else 'AUTH_TOKEN_AUTH'
1720 1719
1721 1720 if ip_access_valid and (
1722 1721 user.is_authenticated or auth_token_access_valid):
1723 1722 log.info('user %s authenticating with:%s IS authenticated on func %s',
1724 1723 user, reason, loc)
1725 1724
1726 1725 return func(*fargs, **fkwargs)
1727 1726 else:
1728 1727 log.warning(
1729 1728 'user %s authenticating with:%s NOT authenticated on '
1730 1729 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s',
1731 1730 user, reason, loc, ip_access_valid, auth_token_access_valid)
1732 1731 # we preserve the get PARAM
1733 1732 came_from = get_came_from(request)
1734 1733
1735 1734 log.debug('redirecting to login page with %s', came_from)
1736 1735 raise HTTPFound(
1737 1736 h.route_path('login', _query={'came_from': came_from}))
1738 1737
1739 1738
1740 1739 class NotAnonymous(object):
1741 1740 """
1742 1741 Must be logged in to execute this function else
1743 1742 redirect to login page
1744 1743 """
1745 1744
1746 1745 def __call__(self, func):
1747 1746 return get_cython_compat_decorator(self.__wrapper, func)
1748 1747
1749 1748 def _get_request(self):
1750 1749 return get_request(self)
1751 1750
1752 1751 def __wrapper(self, func, *fargs, **fkwargs):
1753 1752 import rhodecode.lib.helpers as h
1754 1753 cls = fargs[0]
1755 1754 self.user = cls._rhodecode_user
1756 1755 request = self._get_request()
1757 1756 _ = request.translate
1758 1757 log.debug('Checking if user is not anonymous @%s', cls)
1759 1758
1760 1759 anonymous = self.user.username == User.DEFAULT_USER
1761 1760
1762 1761 if anonymous:
1763 1762 came_from = get_came_from(request)
1764 1763 h.flash(_('You need to be a registered user to '
1765 1764 'perform this action'),
1766 1765 category='warning')
1767 1766 raise HTTPFound(
1768 1767 h.route_path('login', _query={'came_from': came_from}))
1769 1768 else:
1770 1769 return func(*fargs, **fkwargs)
1771 1770
1772 1771
1773 1772 class PermsDecorator(object):
1774 1773 """
1775 1774 Base class for controller decorators, we extract the current user from
1776 1775 the class itself, which has it stored in base controllers
1777 1776 """
1778 1777
1779 1778 def __init__(self, *required_perms):
1780 1779 self.required_perms = set(required_perms)
1781 1780
1782 1781 def __call__(self, func):
1783 1782 return get_cython_compat_decorator(self.__wrapper, func)
1784 1783
1785 1784 def _get_request(self):
1786 1785 return get_request(self)
1787 1786
1788 1787 def __wrapper(self, func, *fargs, **fkwargs):
1789 1788 import rhodecode.lib.helpers as h
1790 1789 cls = fargs[0]
1791 1790 _user = cls._rhodecode_user
1792 1791 request = self._get_request()
1793 1792 _ = request.translate
1794 1793
1795 1794 log.debug('checking %s permissions %s for %s %s',
1796 1795 self.__class__.__name__, self.required_perms, cls, _user)
1797 1796
1798 1797 if self.check_permissions(_user):
1799 1798 log.debug('Permission granted for %s %s', cls, _user)
1800 1799 return func(*fargs, **fkwargs)
1801 1800
1802 1801 else:
1803 1802 log.debug('Permission denied for %s %s', cls, _user)
1804 1803 anonymous = _user.username == User.DEFAULT_USER
1805 1804
1806 1805 if anonymous:
1807 1806 came_from = get_came_from(self._get_request())
1808 1807 h.flash(_('You need to be signed in to view this page'),
1809 1808 category='warning')
1810 1809 raise HTTPFound(
1811 1810 h.route_path('login', _query={'came_from': came_from}))
1812 1811
1813 1812 else:
1814 1813 # redirect with 404 to prevent resource discovery
1815 1814 raise HTTPNotFound()
1816 1815
1817 1816 def check_permissions(self, user):
1818 1817 """Dummy function for overriding"""
1819 1818 raise NotImplementedError(
1820 1819 'You have to write this function in child class')
1821 1820
1822 1821
1823 1822 class HasPermissionAllDecorator(PermsDecorator):
1824 1823 """
1825 1824 Checks for access permission for all given predicates. All of them
1826 1825 have to be meet in order to fulfill the request
1827 1826 """
1828 1827
1829 1828 def check_permissions(self, user):
1830 1829 perms = user.permissions_with_scope({})
1831 1830 if self.required_perms.issubset(perms['global']):
1832 1831 return True
1833 1832 return False
1834 1833
1835 1834
1836 1835 class HasPermissionAnyDecorator(PermsDecorator):
1837 1836 """
1838 1837 Checks for access permission for any of given predicates. In order to
1839 1838 fulfill the request any of predicates must be meet
1840 1839 """
1841 1840
1842 1841 def check_permissions(self, user):
1843 1842 perms = user.permissions_with_scope({})
1844 1843 if self.required_perms.intersection(perms['global']):
1845 1844 return True
1846 1845 return False
1847 1846
1848 1847
1849 1848 class HasRepoPermissionAllDecorator(PermsDecorator):
1850 1849 """
1851 1850 Checks for access permission for all given predicates for specific
1852 1851 repository. All of them have to be meet in order to fulfill the request
1853 1852 """
1854 1853 def _get_repo_name(self):
1855 1854 _request = self._get_request()
1856 1855 return get_repo_slug(_request)
1857 1856
1858 1857 def check_permissions(self, user):
1859 1858 perms = user.permissions
1860 1859 repo_name = self._get_repo_name()
1861 1860
1862 1861 try:
1863 1862 user_perms = {perms['repositories'][repo_name]}
1864 1863 except KeyError:
1865 1864 log.debug('cannot locate repo with name: `%s` in permissions defs',
1866 1865 repo_name)
1867 1866 return False
1868 1867
1869 1868 log.debug('checking `%s` permissions for repo `%s`',
1870 1869 user_perms, repo_name)
1871 1870 if self.required_perms.issubset(user_perms):
1872 1871 return True
1873 1872 return False
1874 1873
1875 1874
1876 1875 class HasRepoPermissionAnyDecorator(PermsDecorator):
1877 1876 """
1878 1877 Checks for access permission for any of given predicates for specific
1879 1878 repository. In order to fulfill the request any of predicates must be meet
1880 1879 """
1881 1880 def _get_repo_name(self):
1882 1881 _request = self._get_request()
1883 1882 return get_repo_slug(_request)
1884 1883
1885 1884 def check_permissions(self, user):
1886 1885 perms = user.permissions
1887 1886 repo_name = self._get_repo_name()
1888 1887
1889 1888 try:
1890 1889 user_perms = {perms['repositories'][repo_name]}
1891 1890 except KeyError:
1892 1891 log.debug(
1893 1892 'cannot locate repo with name: `%s` in permissions defs',
1894 1893 repo_name)
1895 1894 return False
1896 1895
1897 1896 log.debug('checking `%s` permissions for repo `%s`',
1898 1897 user_perms, repo_name)
1899 1898 if self.required_perms.intersection(user_perms):
1900 1899 return True
1901 1900 return False
1902 1901
1903 1902
1904 1903 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1905 1904 """
1906 1905 Checks for access permission for all given predicates for specific
1907 1906 repository group. All of them have to be meet in order to
1908 1907 fulfill the request
1909 1908 """
1910 1909 def _get_repo_group_name(self):
1911 1910 _request = self._get_request()
1912 1911 return get_repo_group_slug(_request)
1913 1912
1914 1913 def check_permissions(self, user):
1915 1914 perms = user.permissions
1916 1915 group_name = self._get_repo_group_name()
1917 1916 try:
1918 1917 user_perms = {perms['repositories_groups'][group_name]}
1919 1918 except KeyError:
1920 1919 log.debug(
1921 1920 'cannot locate repo group with name: `%s` in permissions defs',
1922 1921 group_name)
1923 1922 return False
1924 1923
1925 1924 log.debug('checking `%s` permissions for repo group `%s`',
1926 1925 user_perms, group_name)
1927 1926 if self.required_perms.issubset(user_perms):
1928 1927 return True
1929 1928 return False
1930 1929
1931 1930
1932 1931 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1933 1932 """
1934 1933 Checks for access permission for any of given predicates for specific
1935 1934 repository group. In order to fulfill the request any
1936 1935 of predicates must be met
1937 1936 """
1938 1937 def _get_repo_group_name(self):
1939 1938 _request = self._get_request()
1940 1939 return get_repo_group_slug(_request)
1941 1940
1942 1941 def check_permissions(self, user):
1943 1942 perms = user.permissions
1944 1943 group_name = self._get_repo_group_name()
1945 1944
1946 1945 try:
1947 1946 user_perms = {perms['repositories_groups'][group_name]}
1948 1947 except KeyError:
1949 1948 log.debug(
1950 1949 'cannot locate repo group with name: `%s` in permissions defs',
1951 1950 group_name)
1952 1951 return False
1953 1952
1954 1953 log.debug('checking `%s` permissions for repo group `%s`',
1955 1954 user_perms, group_name)
1956 1955 if self.required_perms.intersection(user_perms):
1957 1956 return True
1958 1957 return False
1959 1958
1960 1959
1961 1960 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1962 1961 """
1963 1962 Checks for access permission for all given predicates for specific
1964 1963 user group. All of them have to be meet in order to fulfill the request
1965 1964 """
1966 1965 def _get_user_group_name(self):
1967 1966 _request = self._get_request()
1968 1967 return get_user_group_slug(_request)
1969 1968
1970 1969 def check_permissions(self, user):
1971 1970 perms = user.permissions
1972 1971 group_name = self._get_user_group_name()
1973 1972 try:
1974 1973 user_perms = {perms['user_groups'][group_name]}
1975 1974 except KeyError:
1976 1975 return False
1977 1976
1978 1977 if self.required_perms.issubset(user_perms):
1979 1978 return True
1980 1979 return False
1981 1980
1982 1981
1983 1982 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1984 1983 """
1985 1984 Checks for access permission for any of given predicates for specific
1986 1985 user group. In order to fulfill the request any of predicates must be meet
1987 1986 """
1988 1987 def _get_user_group_name(self):
1989 1988 _request = self._get_request()
1990 1989 return get_user_group_slug(_request)
1991 1990
1992 1991 def check_permissions(self, user):
1993 1992 perms = user.permissions
1994 1993 group_name = self._get_user_group_name()
1995 1994 try:
1996 1995 user_perms = {perms['user_groups'][group_name]}
1997 1996 except KeyError:
1998 1997 return False
1999 1998
2000 1999 if self.required_perms.intersection(user_perms):
2001 2000 return True
2002 2001 return False
2003 2002
2004 2003
2005 2004 # CHECK FUNCTIONS
2006 2005 class PermsFunction(object):
2007 2006 """Base function for other check functions"""
2008 2007
2009 2008 def __init__(self, *perms):
2010 2009 self.required_perms = set(perms)
2011 2010 self.repo_name = None
2012 2011 self.repo_group_name = None
2013 2012 self.user_group_name = None
2014 2013
2015 2014 def __bool__(self):
2015 import inspect
2016 2016 frame = inspect.currentframe()
2017 2017 stack_trace = traceback.format_stack(frame)
2018 2018 log.error('Checking bool value on a class instance of perm '
2019 2019 'function is not allowed: %s', ''.join(stack_trace))
2020 2020 # rather than throwing errors, here we always return False so if by
2021 2021 # accident someone checks truth for just an instance it will always end
2022 2022 # up in returning False
2023 2023 return False
2024 2024 __nonzero__ = __bool__
2025 2025
2026 2026 def __call__(self, check_location='', user=None):
2027 2027 if not user:
2028 2028 log.debug('Using user attribute from global request')
2029 2029 request = self._get_request()
2030 2030 user = request.user
2031 2031
2032 2032 # init auth user if not already given
2033 2033 if not isinstance(user, AuthUser):
2034 2034 log.debug('Wrapping user %s into AuthUser', user)
2035 2035 user = AuthUser(user.user_id)
2036 2036
2037 2037 cls_name = self.__class__.__name__
2038 2038 check_scope = self._get_check_scope(cls_name)
2039 2039 check_location = check_location or 'unspecified location'
2040 2040
2041 2041 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
2042 2042 self.required_perms, user, check_scope, check_location)
2043 2043 if not user:
2044 2044 log.warning('Empty user given for permission check')
2045 2045 return False
2046 2046
2047 2047 if self.check_permissions(user):
2048 2048 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2049 2049 check_scope, user, check_location)
2050 2050 return True
2051 2051
2052 2052 else:
2053 2053 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2054 2054 check_scope, user, check_location)
2055 2055 return False
2056 2056
2057 2057 def _get_request(self):
2058 2058 return get_request(self)
2059 2059
2060 2060 def _get_check_scope(self, cls_name):
2061 2061 return {
2062 2062 'HasPermissionAll': 'GLOBAL',
2063 2063 'HasPermissionAny': 'GLOBAL',
2064 2064 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
2065 2065 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
2066 2066 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
2067 2067 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
2068 2068 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
2069 2069 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
2070 2070 }.get(cls_name, '?:%s' % cls_name)
2071 2071
2072 2072 def check_permissions(self, user):
2073 2073 """Dummy function for overriding"""
2074 2074 raise Exception('You have to write this function in child class')
2075 2075
2076 2076
2077 2077 class HasPermissionAll(PermsFunction):
2078 2078 def check_permissions(self, user):
2079 2079 perms = user.permissions_with_scope({})
2080 2080 if self.required_perms.issubset(perms.get('global')):
2081 2081 return True
2082 2082 return False
2083 2083
2084 2084
2085 2085 class HasPermissionAny(PermsFunction):
2086 2086 def check_permissions(self, user):
2087 2087 perms = user.permissions_with_scope({})
2088 2088 if self.required_perms.intersection(perms.get('global')):
2089 2089 return True
2090 2090 return False
2091 2091
2092 2092
2093 2093 class HasRepoPermissionAll(PermsFunction):
2094 2094 def __call__(self, repo_name=None, check_location='', user=None):
2095 2095 self.repo_name = repo_name
2096 2096 return super(HasRepoPermissionAll, self).__call__(check_location, user)
2097 2097
2098 2098 def _get_repo_name(self):
2099 2099 if not self.repo_name:
2100 2100 _request = self._get_request()
2101 2101 self.repo_name = get_repo_slug(_request)
2102 2102 return self.repo_name
2103 2103
2104 2104 def check_permissions(self, user):
2105 2105 self.repo_name = self._get_repo_name()
2106 2106 perms = user.permissions
2107 2107 try:
2108 2108 user_perms = {perms['repositories'][self.repo_name]}
2109 2109 except KeyError:
2110 2110 return False
2111 2111 if self.required_perms.issubset(user_perms):
2112 2112 return True
2113 2113 return False
2114 2114
2115 2115
2116 2116 class HasRepoPermissionAny(PermsFunction):
2117 2117 def __call__(self, repo_name=None, check_location='', user=None):
2118 2118 self.repo_name = repo_name
2119 2119 return super(HasRepoPermissionAny, self).__call__(check_location, user)
2120 2120
2121 2121 def _get_repo_name(self):
2122 2122 if not self.repo_name:
2123 2123 _request = self._get_request()
2124 2124 self.repo_name = get_repo_slug(_request)
2125 2125 return self.repo_name
2126 2126
2127 2127 def check_permissions(self, user):
2128 2128 self.repo_name = self._get_repo_name()
2129 2129 perms = user.permissions
2130 2130 try:
2131 2131 user_perms = {perms['repositories'][self.repo_name]}
2132 2132 except KeyError:
2133 2133 return False
2134 2134 if self.required_perms.intersection(user_perms):
2135 2135 return True
2136 2136 return False
2137 2137
2138 2138
2139 2139 class HasRepoGroupPermissionAny(PermsFunction):
2140 2140 def __call__(self, group_name=None, check_location='', user=None):
2141 2141 self.repo_group_name = group_name
2142 2142 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
2143 2143
2144 2144 def check_permissions(self, user):
2145 2145 perms = user.permissions
2146 2146 try:
2147 2147 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2148 2148 except KeyError:
2149 2149 return False
2150 2150 if self.required_perms.intersection(user_perms):
2151 2151 return True
2152 2152 return False
2153 2153
2154 2154
2155 2155 class HasRepoGroupPermissionAll(PermsFunction):
2156 2156 def __call__(self, group_name=None, check_location='', user=None):
2157 2157 self.repo_group_name = group_name
2158 2158 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
2159 2159
2160 2160 def check_permissions(self, user):
2161 2161 perms = user.permissions
2162 2162 try:
2163 2163 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2164 2164 except KeyError:
2165 2165 return False
2166 2166 if self.required_perms.issubset(user_perms):
2167 2167 return True
2168 2168 return False
2169 2169
2170 2170
2171 2171 class HasUserGroupPermissionAny(PermsFunction):
2172 2172 def __call__(self, user_group_name=None, check_location='', user=None):
2173 2173 self.user_group_name = user_group_name
2174 2174 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
2175 2175
2176 2176 def check_permissions(self, user):
2177 2177 perms = user.permissions
2178 2178 try:
2179 2179 user_perms = {perms['user_groups'][self.user_group_name]}
2180 2180 except KeyError:
2181 2181 return False
2182 2182 if self.required_perms.intersection(user_perms):
2183 2183 return True
2184 2184 return False
2185 2185
2186 2186
2187 2187 class HasUserGroupPermissionAll(PermsFunction):
2188 2188 def __call__(self, user_group_name=None, check_location='', user=None):
2189 2189 self.user_group_name = user_group_name
2190 2190 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
2191 2191
2192 2192 def check_permissions(self, user):
2193 2193 perms = user.permissions
2194 2194 try:
2195 2195 user_perms = {perms['user_groups'][self.user_group_name]}
2196 2196 except KeyError:
2197 2197 return False
2198 2198 if self.required_perms.issubset(user_perms):
2199 2199 return True
2200 2200 return False
2201 2201
2202 2202
2203 2203 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2204 2204 class HasPermissionAnyMiddleware(object):
2205 2205 def __init__(self, *perms):
2206 2206 self.required_perms = set(perms)
2207 2207
2208 2208 def __call__(self, auth_user, repo_name):
2209 2209 # repo_name MUST be unicode, since we handle keys in permission
2210 2210 # dict by unicode
2211 2211 repo_name = safe_unicode(repo_name)
2212 2212 log.debug(
2213 2213 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2214 2214 self.required_perms, auth_user, repo_name)
2215 2215
2216 2216 if self.check_permissions(auth_user, repo_name):
2217 2217 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2218 2218 repo_name, auth_user, 'PermissionMiddleware')
2219 2219 return True
2220 2220
2221 2221 else:
2222 2222 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2223 2223 repo_name, auth_user, 'PermissionMiddleware')
2224 2224 return False
2225 2225
2226 2226 def check_permissions(self, user, repo_name):
2227 2227 perms = user.permissions_with_scope({'repo_name': repo_name})
2228 2228
2229 2229 try:
2230 2230 user_perms = {perms['repositories'][repo_name]}
2231 2231 except Exception:
2232 2232 log.exception('Error while accessing user permissions')
2233 2233 return False
2234 2234
2235 2235 if self.required_perms.intersection(user_perms):
2236 2236 return True
2237 2237 return False
2238 2238
2239 2239
2240 2240 # SPECIAL VERSION TO HANDLE API AUTH
2241 2241 class _BaseApiPerm(object):
2242 2242 def __init__(self, *perms):
2243 2243 self.required_perms = set(perms)
2244 2244
2245 2245 def __call__(self, check_location=None, user=None, repo_name=None,
2246 2246 group_name=None, user_group_name=None):
2247 2247 cls_name = self.__class__.__name__
2248 2248 check_scope = 'global:%s' % (self.required_perms,)
2249 2249 if repo_name:
2250 2250 check_scope += ', repo_name:%s' % (repo_name,)
2251 2251
2252 2252 if group_name:
2253 2253 check_scope += ', repo_group_name:%s' % (group_name,)
2254 2254
2255 2255 if user_group_name:
2256 2256 check_scope += ', user_group_name:%s' % (user_group_name,)
2257 2257
2258 2258 log.debug('checking cls:%s %s %s @ %s',
2259 2259 cls_name, self.required_perms, check_scope, check_location)
2260 2260 if not user:
2261 2261 log.debug('Empty User passed into arguments')
2262 2262 return False
2263 2263
2264 2264 # process user
2265 2265 if not isinstance(user, AuthUser):
2266 2266 user = AuthUser(user.user_id)
2267 2267 if not check_location:
2268 2268 check_location = 'unspecified'
2269 2269 if self.check_permissions(user.permissions, repo_name, group_name,
2270 2270 user_group_name):
2271 2271 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2272 2272 check_scope, user, check_location)
2273 2273 return True
2274 2274
2275 2275 else:
2276 2276 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2277 2277 check_scope, user, check_location)
2278 2278 return False
2279 2279
2280 2280 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2281 2281 user_group_name=None):
2282 2282 """
2283 2283 implement in child class should return True if permissions are ok,
2284 2284 False otherwise
2285 2285
2286 2286 :param perm_defs: dict with permission definitions
2287 2287 :param repo_name: repo name
2288 2288 """
2289 2289 raise NotImplementedError()
2290 2290
2291 2291
2292 2292 class HasPermissionAllApi(_BaseApiPerm):
2293 2293 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2294 2294 user_group_name=None):
2295 2295 if self.required_perms.issubset(perm_defs.get('global')):
2296 2296 return True
2297 2297 return False
2298 2298
2299 2299
2300 2300 class HasPermissionAnyApi(_BaseApiPerm):
2301 2301 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2302 2302 user_group_name=None):
2303 2303 if self.required_perms.intersection(perm_defs.get('global')):
2304 2304 return True
2305 2305 return False
2306 2306
2307 2307
2308 2308 class HasRepoPermissionAllApi(_BaseApiPerm):
2309 2309 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2310 2310 user_group_name=None):
2311 2311 try:
2312 2312 _user_perms = {perm_defs['repositories'][repo_name]}
2313 2313 except KeyError:
2314 2314 log.warning(traceback.format_exc())
2315 2315 return False
2316 2316 if self.required_perms.issubset(_user_perms):
2317 2317 return True
2318 2318 return False
2319 2319
2320 2320
2321 2321 class HasRepoPermissionAnyApi(_BaseApiPerm):
2322 2322 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2323 2323 user_group_name=None):
2324 2324 try:
2325 2325 _user_perms = {perm_defs['repositories'][repo_name]}
2326 2326 except KeyError:
2327 2327 log.warning(traceback.format_exc())
2328 2328 return False
2329 2329 if self.required_perms.intersection(_user_perms):
2330 2330 return True
2331 2331 return False
2332 2332
2333 2333
2334 2334 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2335 2335 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2336 2336 user_group_name=None):
2337 2337 try:
2338 2338 _user_perms = {perm_defs['repositories_groups'][group_name]}
2339 2339 except KeyError:
2340 2340 log.warning(traceback.format_exc())
2341 2341 return False
2342 2342 if self.required_perms.intersection(_user_perms):
2343 2343 return True
2344 2344 return False
2345 2345
2346 2346
2347 2347 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2348 2348 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2349 2349 user_group_name=None):
2350 2350 try:
2351 2351 _user_perms = {perm_defs['repositories_groups'][group_name]}
2352 2352 except KeyError:
2353 2353 log.warning(traceback.format_exc())
2354 2354 return False
2355 2355 if self.required_perms.issubset(_user_perms):
2356 2356 return True
2357 2357 return False
2358 2358
2359 2359
2360 2360 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2361 2361 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2362 2362 user_group_name=None):
2363 2363 try:
2364 2364 _user_perms = {perm_defs['user_groups'][user_group_name]}
2365 2365 except KeyError:
2366 2366 log.warning(traceback.format_exc())
2367 2367 return False
2368 2368 if self.required_perms.intersection(_user_perms):
2369 2369 return True
2370 2370 return False
2371 2371
2372 2372
2373 2373 def check_ip_access(source_ip, allowed_ips=None):
2374 2374 """
2375 2375 Checks if source_ip is a subnet of any of allowed_ips.
2376 2376
2377 2377 :param source_ip:
2378 2378 :param allowed_ips: list of allowed ips together with mask
2379 2379 """
2380 2380 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
2381 2381 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2382 2382 if isinstance(allowed_ips, (tuple, list, set)):
2383 2383 for ip in allowed_ips:
2384 2384 ip = safe_unicode(ip)
2385 2385 try:
2386 2386 network_address = ipaddress.ip_network(ip, strict=False)
2387 2387 if source_ip_address in network_address:
2388 2388 log.debug('IP %s is network %s', source_ip_address, network_address)
2389 2389 return True
2390 2390 # for any case we cannot determine the IP, don't crash just
2391 2391 # skip it and log as error, we want to say forbidden still when
2392 2392 # sending bad IP
2393 2393 except Exception:
2394 2394 log.error(traceback.format_exc())
2395 2395 continue
2396 2396 return False
2397 2397
2398 2398
2399 2399 def get_cython_compat_decorator(wrapper, func):
2400 2400 """
2401 2401 Creates a cython compatible decorator. The previously used
2402 2402 decorator.decorator() function seems to be incompatible with cython.
2403 2403
2404 2404 :param wrapper: __wrapper method of the decorator class
2405 2405 :param func: decorated function
2406 2406 """
2407 2407 @wraps(func)
2408 2408 def local_wrapper(*args, **kwds):
2409 2409 return wrapper(func, *args, **kwds)
2410 2410 local_wrapper.__wrapped__ = func
2411 2411 return local_wrapper
2412 2412
2413 2413
General Comments 0
You need to be logged in to leave comments. Login now