##// END OF EJS Templates
auth-tokens: updated logic of authentication to a common shared user method.
marcink -
r1421:5088d9a7 default
parent child Browse files
Show More

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

@@ -1,536 +1,536 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import inspect
22 22 import itertools
23 23 import logging
24 24 import types
25 25 import fnmatch
26 26
27 27 import decorator
28 28 import venusian
29 29 from collections import OrderedDict
30 30
31 31 from pyramid.exceptions import ConfigurationError
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34 from pyramid.httpexceptions import HTTPNotFound
35 35
36 36 from rhodecode.api.exc import (
37 37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 38 from rhodecode.lib.auth import AuthUser
39 39 from rhodecode.lib.base import get_ip_addr
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.utils2 import safe_str
42 42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 43 from rhodecode.model.db import User, UserApiKeys
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 DEFAULT_RENDERER = 'jsonrpc_renderer'
48 48 DEFAULT_URL = '/_admin/apiv2'
49 49
50 50
51 51 def find_methods(jsonrpc_methods, pattern):
52 52 matches = OrderedDict()
53 53 if not isinstance(pattern, (list, tuple)):
54 54 pattern = [pattern]
55 55
56 56 for single_pattern in pattern:
57 57 for method_name, method in jsonrpc_methods.items():
58 58 if fnmatch.fnmatch(method_name, single_pattern):
59 59 matches[method_name] = method
60 60 return matches
61 61
62 62
63 63 class ExtJsonRenderer(object):
64 64 """
65 65 Custom renderer that mkaes use of our ext_json lib
66 66
67 67 """
68 68
69 69 def __init__(self, serializer=json.dumps, **kw):
70 70 """ Any keyword arguments will be passed to the ``serializer``
71 71 function."""
72 72 self.serializer = serializer
73 73 self.kw = kw
74 74
75 75 def __call__(self, info):
76 76 """ Returns a plain JSON-encoded string with content-type
77 77 ``application/json``. The content-type may be overridden by
78 78 setting ``request.response.content_type``."""
79 79
80 80 def _render(value, system):
81 81 request = system.get('request')
82 82 if request is not None:
83 83 response = request.response
84 84 ct = response.content_type
85 85 if ct == response.default_content_type:
86 86 response.content_type = 'application/json'
87 87
88 88 return self.serializer(value, **self.kw)
89 89
90 90 return _render
91 91
92 92
93 93 def jsonrpc_response(request, result):
94 94 rpc_id = getattr(request, 'rpc_id', None)
95 95 response = request.response
96 96
97 97 # store content_type before render is called
98 98 ct = response.content_type
99 99
100 100 ret_value = ''
101 101 if rpc_id:
102 102 ret_value = {
103 103 'id': rpc_id,
104 104 'result': result,
105 105 'error': None,
106 106 }
107 107
108 108 # fetch deprecation warnings, and store it inside results
109 109 deprecation = getattr(request, 'rpc_deprecation', None)
110 110 if deprecation:
111 111 ret_value['DEPRECATION_WARNING'] = deprecation
112 112
113 113 raw_body = render(DEFAULT_RENDERER, ret_value, request=request)
114 114 response.body = safe_str(raw_body, response.charset)
115 115
116 116 if ct == response.default_content_type:
117 117 response.content_type = 'application/json'
118 118
119 119 return response
120 120
121 121
122 122 def jsonrpc_error(request, message, retid=None, code=None):
123 123 """
124 124 Generate a Response object with a JSON-RPC error body
125 125
126 126 :param code:
127 127 :param retid:
128 128 :param message:
129 129 """
130 130 err_dict = {'id': retid, 'result': None, 'error': message}
131 131 body = render(DEFAULT_RENDERER, err_dict, request=request).encode('utf-8')
132 132 return Response(
133 133 body=body,
134 134 status=code,
135 135 content_type='application/json'
136 136 )
137 137
138 138
139 139 def exception_view(exc, request):
140 140 rpc_id = getattr(request, 'rpc_id', None)
141 141
142 142 fault_message = 'undefined error'
143 143 if isinstance(exc, JSONRPCError):
144 144 fault_message = exc.message
145 145 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
146 146 elif isinstance(exc, JSONRPCValidationError):
147 147 colander_exc = exc.colander_exception
148 148 # TODO(marcink): think maybe of nicer way to serialize errors ?
149 149 fault_message = colander_exc.asdict()
150 150 log.debug('json-rpc error rpc_id:%s "%s"', rpc_id, fault_message)
151 151 elif isinstance(exc, JSONRPCForbidden):
152 152 fault_message = 'Access was denied to this resource.'
153 153 log.warning('json-rpc forbidden call rpc_id:%s "%s"', rpc_id, fault_message)
154 154 elif isinstance(exc, HTTPNotFound):
155 155 method = request.rpc_method
156 156 log.debug('json-rpc method `%s` not found in list of '
157 157 'api calls: %s, rpc_id:%s',
158 158 method, request.registry.jsonrpc_methods.keys(), rpc_id)
159 159
160 160 similar = 'none'
161 161 try:
162 162 similar_paterns = ['*{}*'.format(x) for x in method.split('_')]
163 163 similar_found = find_methods(
164 164 request.registry.jsonrpc_methods, similar_paterns)
165 165 similar = ', '.join(similar_found.keys()) or similar
166 166 except Exception:
167 167 # make the whole above block safe
168 168 pass
169 169
170 170 fault_message = "No such method: {}. Similar methods: {}".format(
171 171 method, similar)
172 172
173 173 return jsonrpc_error(request, fault_message, rpc_id)
174 174
175 175
176 176 def request_view(request):
177 177 """
178 178 Main request handling method. It handles all logic to call a specific
179 179 exposed method
180 180 """
181 181
182 182 # check if we can find this session using api_key, get_by_auth_token
183 183 # search not expired tokens only
184 184
185 185 try:
186 186 api_user = User.get_by_auth_token(request.rpc_api_key)
187 187
188 188 if api_user is None:
189 189 return jsonrpc_error(
190 190 request, retid=request.rpc_id, message='Invalid API KEY')
191 191
192 192 if not api_user.active:
193 193 return jsonrpc_error(
194 194 request, retid=request.rpc_id,
195 195 message='Request from this user not allowed')
196 196
197 197 # check if we are allowed to use this IP
198 198 auth_u = AuthUser(
199 199 api_user.user_id, request.rpc_api_key, ip_addr=request.rpc_ip_addr)
200 200 if not auth_u.ip_allowed:
201 201 return jsonrpc_error(
202 202 request, retid=request.rpc_id,
203 203 message='Request from IP:%s not allowed' % (
204 204 request.rpc_ip_addr,))
205 205 else:
206 206 log.info('Access for IP:%s allowed' % (request.rpc_ip_addr,))
207 207
208 208 # register our auth-user
209 209 request.rpc_user = auth_u
210 210
211 211 # now check if token is valid for API
212 role = UserApiKeys.ROLE_API
213 extra_auth_tokens = [
214 x.api_key for x in User.extra_valid_auth_tokens(api_user, role=role)]
215 active_tokens = [api_user.api_key] + extra_auth_tokens
212 auth_token = request.rpc_api_key
213 token_match = api_user.authenticate_by_token(
214 auth_token, roles=[UserApiKeys.ROLE_API], include_builtin_token=True)
215 invalid_token = not token_match
216 216
217 log.debug('Checking if API key has proper role')
218 if request.rpc_api_key not in active_tokens:
217 log.debug('Checking if API KEY is valid with proper role')
218 if invalid_token:
219 219 return jsonrpc_error(
220 220 request, retid=request.rpc_id,
221 message='API KEY has bad role for an API call')
221 message='API KEY invalid or, has bad role for an API call')
222 222
223 except Exception as e:
223 except Exception:
224 224 log.exception('Error on API AUTH')
225 225 return jsonrpc_error(
226 226 request, retid=request.rpc_id, message='Invalid API KEY')
227 227
228 228 method = request.rpc_method
229 229 func = request.registry.jsonrpc_methods[method]
230 230
231 231 # now that we have a method, add request._req_params to
232 232 # self.kargs and dispatch control to WGIController
233 233 argspec = inspect.getargspec(func)
234 234 arglist = argspec[0]
235 235 defaults = map(type, argspec[3] or [])
236 236 default_empty = types.NotImplementedType
237 237
238 238 # kw arguments required by this method
239 239 func_kwargs = dict(itertools.izip_longest(
240 240 reversed(arglist), reversed(defaults), fillvalue=default_empty))
241 241
242 242 # This attribute will need to be first param of a method that uses
243 243 # api_key, which is translated to instance of user at that name
244 244 user_var = 'apiuser'
245 245 request_var = 'request'
246 246
247 247 for arg in [user_var, request_var]:
248 248 if arg not in arglist:
249 249 return jsonrpc_error(
250 250 request,
251 251 retid=request.rpc_id,
252 252 message='This method [%s] does not support '
253 253 'required parameter `%s`' % (func.__name__, arg))
254 254
255 255 # get our arglist and check if we provided them as args
256 256 for arg, default in func_kwargs.items():
257 257 if arg in [user_var, request_var]:
258 258 # user_var and request_var are pre-hardcoded parameters and we
259 259 # don't need to do any translation
260 260 continue
261 261
262 262 # skip the required param check if it's default value is
263 263 # NotImplementedType (default_empty)
264 264 if default == default_empty and arg not in request.rpc_params:
265 265 return jsonrpc_error(
266 266 request,
267 267 retid=request.rpc_id,
268 268 message=('Missing non optional `%s` arg in JSON DATA' % arg)
269 269 )
270 270
271 271 # sanitize extra passed arguments
272 272 for k in request.rpc_params.keys()[:]:
273 273 if k not in func_kwargs:
274 274 del request.rpc_params[k]
275 275
276 276 call_params = request.rpc_params
277 277 call_params.update({
278 278 'request': request,
279 279 'apiuser': auth_u
280 280 })
281 281 try:
282 282 ret_value = func(**call_params)
283 283 return jsonrpc_response(request, ret_value)
284 284 except JSONRPCBaseError:
285 285 raise
286 286 except Exception:
287 287 log.exception('Unhandled exception occurred on api call: %s', func)
288 288 return jsonrpc_error(request, retid=request.rpc_id,
289 289 message='Internal server error')
290 290
291 291
292 292 def setup_request(request):
293 293 """
294 294 Parse a JSON-RPC request body. It's used inside the predicates method
295 295 to validate and bootstrap requests for usage in rpc calls.
296 296
297 297 We need to raise JSONRPCError here if we want to return some errors back to
298 298 user.
299 299 """
300 300
301 301 log.debug('Executing setup request: %r', request)
302 302 request.rpc_ip_addr = get_ip_addr(request.environ)
303 303 # TODO(marcink): deprecate GET at some point
304 304 if request.method not in ['POST', 'GET']:
305 305 log.debug('unsupported request method "%s"', request.method)
306 306 raise JSONRPCError(
307 307 'unsupported request method "%s". Please use POST' % request.method)
308 308
309 309 if 'CONTENT_LENGTH' not in request.environ:
310 310 log.debug("No Content-Length")
311 311 raise JSONRPCError("Empty body, No Content-Length in request")
312 312
313 313 else:
314 314 length = request.environ['CONTENT_LENGTH']
315 315 log.debug('Content-Length: %s', length)
316 316
317 317 if length == 0:
318 318 log.debug("Content-Length is 0")
319 319 raise JSONRPCError("Content-Length is 0")
320 320
321 321 raw_body = request.body
322 322 try:
323 323 json_body = json.loads(raw_body)
324 324 except ValueError as e:
325 325 # catch JSON errors Here
326 326 raise JSONRPCError("JSON parse error ERR:%s RAW:%r" % (e, raw_body))
327 327
328 328 request.rpc_id = json_body.get('id')
329 329 request.rpc_method = json_body.get('method')
330 330
331 331 # check required base parameters
332 332 try:
333 333 api_key = json_body.get('api_key')
334 334 if not api_key:
335 335 api_key = json_body.get('auth_token')
336 336
337 337 if not api_key:
338 338 raise KeyError('api_key or auth_token')
339 339
340 340 # TODO(marcink): support passing in token in request header
341 341
342 342 request.rpc_api_key = api_key
343 343 request.rpc_id = json_body['id']
344 344 request.rpc_method = json_body['method']
345 345 request.rpc_params = json_body['args'] \
346 346 if isinstance(json_body['args'], dict) else {}
347 347
348 348 log.debug(
349 349 'method: %s, params: %s' % (request.rpc_method, request.rpc_params))
350 350 except KeyError as e:
351 351 raise JSONRPCError('Incorrect JSON data. Missing %s' % e)
352 352
353 353 log.debug('setup complete, now handling method:%s rpcid:%s',
354 354 request.rpc_method, request.rpc_id, )
355 355
356 356
357 357 class RoutePredicate(object):
358 358 def __init__(self, val, config):
359 359 self.val = val
360 360
361 361 def text(self):
362 362 return 'jsonrpc route = %s' % self.val
363 363
364 364 phash = text
365 365
366 366 def __call__(self, info, request):
367 367 if self.val:
368 368 # potentially setup and bootstrap our call
369 369 setup_request(request)
370 370
371 371 # Always return True so that even if it isn't a valid RPC it
372 372 # will fall through to the underlaying handlers like notfound_view
373 373 return True
374 374
375 375
376 376 class NotFoundPredicate(object):
377 377 def __init__(self, val, config):
378 378 self.val = val
379 379 self.methods = config.registry.jsonrpc_methods
380 380
381 381 def text(self):
382 382 return 'jsonrpc method not found = {}.'.format(self.val)
383 383
384 384 phash = text
385 385
386 386 def __call__(self, info, request):
387 387 return hasattr(request, 'rpc_method')
388 388
389 389
390 390 class MethodPredicate(object):
391 391 def __init__(self, val, config):
392 392 self.method = val
393 393
394 394 def text(self):
395 395 return 'jsonrpc method = %s' % self.method
396 396
397 397 phash = text
398 398
399 399 def __call__(self, context, request):
400 400 # we need to explicitly return False here, so pyramid doesn't try to
401 401 # execute our view directly. We need our main handler to execute things
402 402 return getattr(request, 'rpc_method') == self.method
403 403
404 404
405 405 def add_jsonrpc_method(config, view, **kwargs):
406 406 # pop the method name
407 407 method = kwargs.pop('method', None)
408 408
409 409 if method is None:
410 410 raise ConfigurationError(
411 411 'Cannot register a JSON-RPC method without specifying the '
412 412 '"method"')
413 413
414 414 # we define custom predicate, to enable to detect conflicting methods,
415 415 # those predicates are kind of "translation" from the decorator variables
416 416 # to internal predicates names
417 417
418 418 kwargs['jsonrpc_method'] = method
419 419
420 420 # register our view into global view store for validation
421 421 config.registry.jsonrpc_methods[method] = view
422 422
423 423 # we're using our main request_view handler, here, so each method
424 424 # has a unified handler for itself
425 425 config.add_view(request_view, route_name='apiv2', **kwargs)
426 426
427 427
428 428 class jsonrpc_method(object):
429 429 """
430 430 decorator that works similar to @add_view_config decorator,
431 431 but tailored for our JSON RPC
432 432 """
433 433
434 434 venusian = venusian # for testing injection
435 435
436 436 def __init__(self, method=None, **kwargs):
437 437 self.method = method
438 438 self.kwargs = kwargs
439 439
440 440 def __call__(self, wrapped):
441 441 kwargs = self.kwargs.copy()
442 442 kwargs['method'] = self.method or wrapped.__name__
443 443 depth = kwargs.pop('_depth', 0)
444 444
445 445 def callback(context, name, ob):
446 446 config = context.config.with_package(info.module)
447 447 config.add_jsonrpc_method(view=ob, **kwargs)
448 448
449 449 info = venusian.attach(wrapped, callback, category='pyramid',
450 450 depth=depth + 1)
451 451 if info.scope == 'class':
452 452 # ensure that attr is set if decorating a class method
453 453 kwargs.setdefault('attr', wrapped.__name__)
454 454
455 455 kwargs['_info'] = info.codeinfo # fbo action_method
456 456 return wrapped
457 457
458 458
459 459 class jsonrpc_deprecated_method(object):
460 460 """
461 461 Marks method as deprecated, adds log.warning, and inject special key to
462 462 the request variable to mark method as deprecated.
463 463 Also injects special docstring that extract_docs will catch to mark
464 464 method as deprecated.
465 465
466 466 :param use_method: specify which method should be used instead of
467 467 the decorated one
468 468
469 469 Use like::
470 470
471 471 @jsonrpc_method()
472 472 @jsonrpc_deprecated_method(use_method='new_func', deprecated_at_version='3.0.0')
473 473 def old_func(request, apiuser, arg1, arg2):
474 474 ...
475 475 """
476 476
477 477 def __init__(self, use_method, deprecated_at_version):
478 478 self.use_method = use_method
479 479 self.deprecated_at_version = deprecated_at_version
480 480 self.deprecated_msg = ''
481 481
482 482 def __call__(self, func):
483 483 self.deprecated_msg = 'Please use method `{method}` instead.'.format(
484 484 method=self.use_method)
485 485
486 486 docstring = """\n
487 487 .. deprecated:: {version}
488 488
489 489 {deprecation_message}
490 490
491 491 {original_docstring}
492 492 """
493 493 func.__doc__ = docstring.format(
494 494 version=self.deprecated_at_version,
495 495 deprecation_message=self.deprecated_msg,
496 496 original_docstring=func.__doc__)
497 497 return decorator.decorator(self.__wrapper, func)
498 498
499 499 def __wrapper(self, func, *fargs, **fkwargs):
500 500 log.warning('DEPRECATED API CALL on function %s, please '
501 501 'use `%s` instead', func, self.use_method)
502 502 # alter function docstring to mark as deprecated, this is picked up
503 503 # via fabric file that generates API DOC.
504 504 result = func(*fargs, **fkwargs)
505 505
506 506 request = fargs[0]
507 507 request.rpc_deprecation = 'DEPRECATED METHOD ' + self.deprecated_msg
508 508 return result
509 509
510 510
511 511 def includeme(config):
512 512 plugin_module = 'rhodecode.api'
513 513 plugin_settings = get_plugin_settings(
514 514 plugin_module, config.registry.settings)
515 515
516 516 if not hasattr(config.registry, 'jsonrpc_methods'):
517 517 config.registry.jsonrpc_methods = OrderedDict()
518 518
519 519 # match filter by given method only
520 520 config.add_view_predicate('jsonrpc_method', MethodPredicate)
521 521
522 522 config.add_renderer(DEFAULT_RENDERER, ExtJsonRenderer(
523 523 serializer=json.dumps, indent=4))
524 524 config.add_directive('add_jsonrpc_method', add_jsonrpc_method)
525 525
526 526 config.add_route_predicate(
527 527 'jsonrpc_call', RoutePredicate)
528 528
529 529 config.add_route(
530 530 'apiv2', plugin_settings.get('url', DEFAULT_URL), jsonrpc_call=True)
531 531
532 532 config.scan(plugin_module, ignore='rhodecode.api.tests')
533 533 # register some exception handling view
534 534 config.add_view(exception_view, context=JSONRPCBaseError)
535 535 config.add_view_predicate('jsonrpc_method_not_found', NotFoundPredicate)
536 536 config.add_notfound_view(exception_view, jsonrpc_method_not_found=True)
@@ -1,140 +1,140 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 RhodeCode authentication token plugin for built in internal auth
23 23 """
24 24
25 25 import logging
26 26
27 27 from sqlalchemy.ext.hybrid import hybrid_property
28 28
29 29 from rhodecode.translation import _
30 30 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, VCS_TYPE
31 31 from rhodecode.authentication.routes import AuthnPluginResourceBase
32 32 from rhodecode.model.db import User, UserApiKeys
33 33
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def plugin_factory(plugin_id, *args, **kwds):
39 39 plugin = RhodeCodeAuthPlugin(plugin_id)
40 40 return plugin
41 41
42 42
43 43 class RhodecodeAuthnResource(AuthnPluginResourceBase):
44 44 pass
45 45
46 46
47 47 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
48 48 """
49 49 Enables usage of authentication tokens for vcs operations.
50 50 """
51 51
52 52 def includeme(self, config):
53 53 config.add_authn_plugin(self)
54 54 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
55 55 config.add_view(
56 56 'rhodecode.authentication.views.AuthnPluginViewBase',
57 57 attr='settings_get',
58 58 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
59 59 request_method='GET',
60 60 route_name='auth_home',
61 61 context=RhodecodeAuthnResource)
62 62 config.add_view(
63 63 'rhodecode.authentication.views.AuthnPluginViewBase',
64 64 attr='settings_post',
65 65 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
66 66 request_method='POST',
67 67 route_name='auth_home',
68 68 context=RhodecodeAuthnResource)
69 69
70 70 def get_display_name(self):
71 71 return _('Rhodecode Token Auth')
72 72
73 73 @hybrid_property
74 74 def name(self):
75 75 return "authtoken"
76 76
77 77 def user_activation_state(self):
78 78 def_user_perms = User.get_default_user().AuthUser.permissions['global']
79 79 return 'hg.register.auto_activate' in def_user_perms
80 80
81 81 def allows_authentication_from(
82 82 self, user, allows_non_existing_user=True,
83 83 allowed_auth_plugins=None, allowed_auth_sources=None):
84 84 """
85 85 Custom method for this auth that doesn't accept empty users. And also
86 86 allows users from all other active plugins to use it and also
87 87 authenticate against it. But only via vcs mode
88 88 """
89 89 from rhodecode.authentication.base import get_authn_registry
90 90 authn_registry = get_authn_registry()
91 91
92 92 active_plugins = set(
93 93 [x.name for x in authn_registry.get_plugins_for_authentication()])
94 94 active_plugins.discard(self.name)
95 95
96 96 allowed_auth_plugins = [self.name] + list(active_plugins)
97 97 # only for vcs operations
98 98 allowed_auth_sources = [VCS_TYPE]
99 99
100 100 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
101 101 user, allows_non_existing_user=False,
102 102 allowed_auth_plugins=allowed_auth_plugins,
103 103 allowed_auth_sources=allowed_auth_sources)
104 104
105 105 def auth(self, userobj, username, password, settings, **kwargs):
106 106 if not userobj:
107 107 log.debug('userobj was:%s skipping' % (userobj, ))
108 108 return None
109 109
110 110 user_attrs = {
111 111 "username": userobj.username,
112 112 "firstname": userobj.firstname,
113 113 "lastname": userobj.lastname,
114 114 "groups": [],
115 115 "email": userobj.email,
116 116 "admin": userobj.admin,
117 117 "active": userobj.active,
118 118 "active_from_extern": userobj.active,
119 119 "extern_name": userobj.user_id,
120 120 "extern_type": userobj.extern_type,
121 121 }
122 122
123 123 log.debug('Authenticating user with args %s', user_attrs)
124 124 if userobj.active:
125 role = UserApiKeys.ROLE_VCS
126 active_tokens = [x.api_key for x in
127 User.extra_valid_auth_tokens(userobj, role=role)]
128 if userobj.username == username and password in active_tokens:
125 token_match = userobj.authenticate_by_token(
126 password, roles=[UserApiKeys.ROLE_VCS])
127
128 if userobj.username == username and token_match:
129 129 log.info(
130 130 'user `%s` successfully authenticated via %s',
131 131 user_attrs['username'], self.name)
132 132 return user_attrs
133 133 log.error(
134 134 'user `%s` failed to authenticate via %s, reason: bad or '
135 135 'inactive token.', username, self.name)
136 136 else:
137 137 log.warning(
138 138 'user `%s` failed to authenticate via %s, reason: account not '
139 139 'active.', username, self.name)
140 140 return None
@@ -1,181 +1,180 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Feed controller for RhodeCode
23 23 """
24 24
25 25 import logging
26 26
27 27 import pytz
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 from beaker.cache import cache_region, region_invalidate
31 from beaker.cache import cache_region
32 32 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
33 33
34 from rhodecode.model.db import CacheKey
34 from rhodecode.model.db import CacheKey, UserApiKeys
35 35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import caches
37 36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 37 from rhodecode.lib.base import BaseRepoController
39 38 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
40 39 from rhodecode.lib.utils2 import safe_int, str2bool
41 40 from rhodecode.lib.utils import PartialRenderer
42 41
43 42 log = logging.getLogger(__name__)
44 43
45 44
46 45 class FeedController(BaseRepoController):
47 46
48 47 def _get_config(self):
49 48 import rhodecode
50 49 config = rhodecode.CONFIG
51 50
52 51 return {
53 52 'language': 'en-us',
54 53 'feed_ttl': '5', # TTL of feed,
55 54 'feed_include_diff':
56 55 str2bool(config.get('rss_include_diff', False)),
57 56 'feed_items_per_page':
58 57 safe_int(config.get('rss_items_per_page', 20)),
59 58 'feed_diff_limit':
60 59 # we need to protect from parsing huge diffs here other way
61 60 # we can kill the server
62 61 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
63 62 }
64 63
65 @LoginRequired(auth_token_access=True)
64 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
66 65 def __before__(self):
67 66 super(FeedController, self).__before__()
68 67 config = self._get_config()
69 68 # common values for feeds
70 69 self.description = _('Changes on %s repository')
71 70 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
72 71 self.language = config["language"]
73 72 self.ttl = config["feed_ttl"]
74 73 self.feed_include_diff = config['feed_include_diff']
75 74 self.feed_diff_limit = config['feed_diff_limit']
76 75 self.feed_items_per_page = config['feed_items_per_page']
77 76
78 77 def __changes(self, commit):
79 78 diff_processor = DiffProcessor(
80 79 commit.diff(), diff_limit=self.feed_diff_limit)
81 80 _parsed = diff_processor.prepare(inline_diff=False)
82 81 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83 82
84 83 return _parsed, limited_diff
85 84
86 85 def _get_title(self, commit):
87 86 return h.shorter(commit.message, 160)
88 87
89 88 def _get_description(self, commit):
90 89 _renderer = PartialRenderer('feed/atom_feed_entry.mako')
91 90 parsed_diff, limited_diff = self.__changes(commit)
92 91 return _renderer(
93 92 'body',
94 93 commit=commit,
95 94 parsed_diff=parsed_diff,
96 95 limited_diff=limited_diff,
97 96 feed_include_diff=self.feed_include_diff,
98 97 )
99 98
100 99 def _set_timezone(self, date, tzinfo=pytz.utc):
101 100 if not getattr(date, "tzinfo", None):
102 101 date.replace(tzinfo=tzinfo)
103 102 return date
104 103
105 104 def _get_commits(self):
106 105 return list(c.rhodecode_repo[-self.feed_items_per_page:])
107 106
108 107 @HasRepoPermissionAnyDecorator(
109 108 'repository.read', 'repository.write', 'repository.admin')
110 109 def atom(self, repo_name):
111 110 """Produce an atom-1.0 feed via feedgenerator module"""
112 111
113 112 @cache_region('long_term')
114 113 def _generate_feed(cache_key):
115 114 feed = Atom1Feed(
116 115 title=self.title % repo_name,
117 116 link=url('summary_home', repo_name=repo_name, qualified=True),
118 117 description=self.description % repo_name,
119 118 language=self.language,
120 119 ttl=self.ttl
121 120 )
122 121
123 122 for commit in reversed(self._get_commits()):
124 123 date = self._set_timezone(commit.date)
125 124 feed.add_item(
126 125 title=self._get_title(commit),
127 126 author_name=commit.author,
128 127 description=self._get_description(commit),
129 128 link=url('changeset_home', repo_name=repo_name,
130 129 revision=commit.raw_id, qualified=True),
131 130 pubdate=date,)
132 131
133 132 return feed.mime_type, feed.writeString('utf-8')
134 133
135 134 invalidator_context = CacheKey.repo_context_cache(
136 135 _generate_feed, repo_name, CacheKey.CACHE_TYPE_ATOM)
137 136
138 137 with invalidator_context as context:
139 138 context.invalidate()
140 139 mime_type, feed = context.compute()
141 140
142 141 response.content_type = mime_type
143 142 return feed
144 143
145 144 @HasRepoPermissionAnyDecorator(
146 145 'repository.read', 'repository.write', 'repository.admin')
147 146 def rss(self, repo_name):
148 147 """Produce an rss2 feed via feedgenerator module"""
149 148
150 149 @cache_region('long_term')
151 150 def _generate_feed(cache_key):
152 151 feed = Rss201rev2Feed(
153 152 title=self.title % repo_name,
154 153 link=url('summary_home', repo_name=repo_name,
155 154 qualified=True),
156 155 description=self.description % repo_name,
157 156 language=self.language,
158 157 ttl=self.ttl
159 158 )
160 159
161 160 for commit in reversed(self._get_commits()):
162 161 date = self._set_timezone(commit.date)
163 162 feed.add_item(
164 163 title=self._get_title(commit),
165 164 author_name=commit.author,
166 165 description=self._get_description(commit),
167 166 link=url('changeset_home', repo_name=repo_name,
168 167 revision=commit.raw_id, qualified=True),
169 168 pubdate=date,)
170 169
171 170 return feed.mime_type, feed.writeString('utf-8')
172 171
173 172 invalidator_context = CacheKey.repo_context_cache(
174 173 _generate_feed, repo_name, CacheKey.CACHE_TYPE_RSS)
175 174
176 175 with invalidator_context as context:
177 176 context.invalidate()
178 177 mime_type, feed = context.compute()
179 178
180 179 response.content_type = mime_type
181 180 return feed
@@ -1,306 +1,306 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Journal / user event log controller for rhodecode
23 23 """
24 24
25 25 import logging
26 26 from itertools import groupby
27 27
28 28 from sqlalchemy import or_
29 29 from sqlalchemy.orm import joinedload
30 30
31 31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 32
33 33 from webob.exc import HTTPBadRequest
34 34 from pylons import request, tmpl_context as c, response, url
35 35 from pylons.i18n.translation import _
36 36
37 37 from rhodecode.controllers.admin.admin import _journal_filter
38 from rhodecode.model.db import UserLog, UserFollowing, User
38 from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys
39 39 from rhodecode.model.meta import Session
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.helpers import Page
42 42 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
43 43 from rhodecode.lib.base import BaseController, render
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class JournalController(BaseController):
50 50
51 51 def __before__(self):
52 52 super(JournalController, self).__before__()
53 53 self.language = 'en-us'
54 54 self.ttl = "5"
55 55 self.feed_nr = 20
56 56 c.search_term = request.GET.get('filter')
57 57
58 58 def _get_daily_aggregate(self, journal):
59 59 groups = []
60 60 for k, g in groupby(journal, lambda x: x.action_as_day):
61 61 user_group = []
62 62 #groupby username if it's a present value, else fallback to journal username
63 63 for _, g2 in groupby(list(g), lambda x: x.user.username if x.user else x.username):
64 64 l = list(g2)
65 65 user_group.append((l[0].user, l))
66 66
67 67 groups.append((k, user_group,))
68 68
69 69 return groups
70 70
71 71 def _get_journal_data(self, following_repos):
72 72 repo_ids = [x.follows_repository.repo_id for x in following_repos
73 73 if x.follows_repository is not None]
74 74 user_ids = [x.follows_user.user_id for x in following_repos
75 75 if x.follows_user is not None]
76 76
77 77 filtering_criterion = None
78 78
79 79 if repo_ids and user_ids:
80 80 filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
81 81 UserLog.user_id.in_(user_ids))
82 82 if repo_ids and not user_ids:
83 83 filtering_criterion = UserLog.repository_id.in_(repo_ids)
84 84 if not repo_ids and user_ids:
85 85 filtering_criterion = UserLog.user_id.in_(user_ids)
86 86 if filtering_criterion is not None:
87 87 journal = self.sa.query(UserLog)\
88 88 .options(joinedload(UserLog.user))\
89 89 .options(joinedload(UserLog.repository))
90 90 #filter
91 91 try:
92 92 journal = _journal_filter(journal, c.search_term)
93 93 except Exception:
94 94 # we want this to crash for now
95 95 raise
96 96 journal = journal.filter(filtering_criterion)\
97 97 .order_by(UserLog.action_date.desc())
98 98 else:
99 99 journal = []
100 100
101 101 return journal
102 102
103 103 def _atom_feed(self, repos, public=True):
104 104 journal = self._get_journal_data(repos)
105 105 if public:
106 106 _link = url('public_journal_atom', qualified=True)
107 107 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
108 108 'atom feed')
109 109 else:
110 110 _link = url('journal_atom', qualified=True)
111 111 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'atom feed')
112 112
113 113 feed = Atom1Feed(title=_desc,
114 114 link=_link,
115 115 description=_desc,
116 116 language=self.language,
117 117 ttl=self.ttl)
118 118
119 119 for entry in journal[:self.feed_nr]:
120 120 user = entry.user
121 121 if user is None:
122 122 #fix deleted users
123 123 user = AttributeDict({'short_contact': entry.username,
124 124 'email': '',
125 125 'full_contact': ''})
126 126 action, action_extra, ico = h.action_parser(entry, feed=True)
127 127 title = "%s - %s %s" % (user.short_contact, action(),
128 128 entry.repository.repo_name)
129 129 desc = action_extra()
130 130 _url = None
131 131 if entry.repository is not None:
132 132 _url = url('changelog_home',
133 133 repo_name=entry.repository.repo_name,
134 134 qualified=True)
135 135
136 136 feed.add_item(title=title,
137 137 pubdate=entry.action_date,
138 138 link=_url or url('', qualified=True),
139 139 author_email=user.email,
140 140 author_name=user.full_contact,
141 141 description=desc)
142 142
143 143 response.content_type = feed.mime_type
144 144 return feed.writeString('utf-8')
145 145
146 146 def _rss_feed(self, repos, public=True):
147 147 journal = self._get_journal_data(repos)
148 148 if public:
149 149 _link = url('public_journal_atom', qualified=True)
150 150 _desc = '%s %s %s' % (c.rhodecode_name, _('public journal'),
151 151 'rss feed')
152 152 else:
153 153 _link = url('journal_atom', qualified=True)
154 154 _desc = '%s %s %s' % (c.rhodecode_name, _('journal'), 'rss feed')
155 155
156 156 feed = Rss201rev2Feed(title=_desc,
157 157 link=_link,
158 158 description=_desc,
159 159 language=self.language,
160 160 ttl=self.ttl)
161 161
162 162 for entry in journal[:self.feed_nr]:
163 163 user = entry.user
164 164 if user is None:
165 165 #fix deleted users
166 166 user = AttributeDict({'short_contact': entry.username,
167 167 'email': '',
168 168 'full_contact': ''})
169 169 action, action_extra, ico = h.action_parser(entry, feed=True)
170 170 title = "%s - %s %s" % (user.short_contact, action(),
171 171 entry.repository.repo_name)
172 172 desc = action_extra()
173 173 _url = None
174 174 if entry.repository is not None:
175 175 _url = url('changelog_home',
176 176 repo_name=entry.repository.repo_name,
177 177 qualified=True)
178 178
179 179 feed.add_item(title=title,
180 180 pubdate=entry.action_date,
181 181 link=_url or url('', qualified=True),
182 182 author_email=user.email,
183 183 author_name=user.full_contact,
184 184 description=desc)
185 185
186 186 response.content_type = feed.mime_type
187 187 return feed.writeString('utf-8')
188 188
189 189 @LoginRequired()
190 190 @NotAnonymous()
191 191 def index(self):
192 192 # Return a rendered template
193 193 p = safe_int(request.GET.get('page', 1), 1)
194 194 c.user = User.get(c.rhodecode_user.user_id)
195 195 following = self.sa.query(UserFollowing)\
196 196 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
197 197 .options(joinedload(UserFollowing.follows_repository))\
198 198 .all()
199 199
200 200 journal = self._get_journal_data(following)
201 201
202 202 def url_generator(**kw):
203 203 return url.current(filter=c.search_term, **kw)
204 204
205 205 c.journal_pager = Page(journal, page=p, items_per_page=20, url=url_generator)
206 206 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
207 207
208 208 c.journal_data = render('journal/journal_data.mako')
209 209 if request.is_xhr:
210 210 return c.journal_data
211 211
212 212 return render('journal/journal.mako')
213 213
214 @LoginRequired(auth_token_access=True)
214 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
215 215 @NotAnonymous()
216 216 def journal_atom(self):
217 217 """
218 218 Produce an atom-1.0 feed via feedgenerator module
219 219 """
220 220 following = self.sa.query(UserFollowing)\
221 221 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
222 222 .options(joinedload(UserFollowing.follows_repository))\
223 223 .all()
224 224 return self._atom_feed(following, public=False)
225 225
226 @LoginRequired(auth_token_access=True)
226 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
227 227 @NotAnonymous()
228 228 def journal_rss(self):
229 229 """
230 230 Produce an rss feed via feedgenerator module
231 231 """
232 232 following = self.sa.query(UserFollowing)\
233 233 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
234 234 .options(joinedload(UserFollowing.follows_repository))\
235 235 .all()
236 236 return self._rss_feed(following, public=False)
237 237
238 238 @CSRFRequired()
239 239 @LoginRequired()
240 240 @NotAnonymous()
241 241 def toggle_following(self):
242 242 user_id = request.POST.get('follows_user_id')
243 243 if user_id:
244 244 try:
245 245 self.scm_model.toggle_following_user(
246 246 user_id, c.rhodecode_user.user_id)
247 247 Session().commit()
248 248 return 'ok'
249 249 except Exception:
250 250 raise HTTPBadRequest()
251 251
252 252 repo_id = request.POST.get('follows_repo_id')
253 253 if repo_id:
254 254 try:
255 255 self.scm_model.toggle_following_repo(
256 256 repo_id, c.rhodecode_user.user_id)
257 257 Session().commit()
258 258 return 'ok'
259 259 except Exception:
260 260 raise HTTPBadRequest()
261 261
262 262
263 263 @LoginRequired()
264 264 def public_journal(self):
265 265 # Return a rendered template
266 266 p = safe_int(request.GET.get('page', 1), 1)
267 267
268 268 c.following = self.sa.query(UserFollowing)\
269 269 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
270 270 .options(joinedload(UserFollowing.follows_repository))\
271 271 .all()
272 272
273 273 journal = self._get_journal_data(c.following)
274 274
275 275 c.journal_pager = Page(journal, page=p, items_per_page=20)
276 276
277 277 c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
278 278
279 279 c.journal_data = render('journal/journal_data.mako')
280 280 if request.is_xhr:
281 281 return c.journal_data
282 282 return render('journal/public_journal.mako')
283 283
284 @LoginRequired(auth_token_access=True)
284 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
285 285 def public_journal_atom(self):
286 286 """
287 287 Produce an atom-1.0 feed via feedgenerator module
288 288 """
289 289 c.following = self.sa.query(UserFollowing)\
290 290 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
291 291 .options(joinedload(UserFollowing.follows_repository))\
292 292 .all()
293 293
294 294 return self._atom_feed(c.following)
295 295
296 @LoginRequired(auth_token_access=True)
296 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
297 297 def public_journal_rss(self):
298 298 """
299 299 Produce an rss2 feed via feedgenerator module
300 300 """
301 301 c.following = self.sa.query(UserFollowing)\
302 302 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
303 303 .options(joinedload(UserFollowing.follows_repository))\
304 304 .all()
305 305
306 306 return self._rss_feed(c.following)
@@ -1,1907 +1,1909 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 authentication and permission libraries
23 23 """
24 24
25 25 import inspect
26 26 import collections
27 27 import fnmatch
28 28 import hashlib
29 29 import itertools
30 30 import logging
31 31 import os
32 32 import random
33 33 import time
34 34 import traceback
35 35 from functools import wraps
36 36
37 37 import ipaddress
38 38 from pyramid.httpexceptions import HTTPForbidden
39 39 from pylons import url, request
40 40 from pylons.controllers.util import abort, redirect
41 41 from pylons.i18n.translation import _
42 42 from sqlalchemy import or_
43 43 from sqlalchemy.orm.exc import ObjectDeletedError
44 44 from sqlalchemy.orm import joinedload
45 45 from zope.cachedescriptors.property import Lazy as LazyProperty
46 46
47 47 import rhodecode
48 48 from rhodecode.model import meta
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.user import UserModel
51 51 from rhodecode.model.db import (
52 52 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
53 53 UserIpMap, UserApiKeys, RepoGroup)
54 54 from rhodecode.lib import caches
55 55 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5
56 56 from rhodecode.lib.utils import (
57 57 get_repo_slug, get_repo_group_slug, get_user_group_slug)
58 58 from rhodecode.lib.caching_query import FromCache
59 59
60 60
61 61 if rhodecode.is_unix:
62 62 import bcrypt
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66 csrf_token_key = "csrf_token"
67 67
68 68
69 69 class PasswordGenerator(object):
70 70 """
71 71 This is a simple class for generating password from different sets of
72 72 characters
73 73 usage::
74 74
75 75 passwd_gen = PasswordGenerator()
76 76 #print 8-letter password containing only big and small letters
77 77 of alphabet
78 78 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
79 79 """
80 80 ALPHABETS_NUM = r'''1234567890'''
81 81 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
82 82 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
83 83 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
84 84 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
85 85 + ALPHABETS_NUM + ALPHABETS_SPECIAL
86 86 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
87 87 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
88 88 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
89 89 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
90 90
91 91 def __init__(self, passwd=''):
92 92 self.passwd = passwd
93 93
94 94 def gen_password(self, length, type_=None):
95 95 if type_ is None:
96 96 type_ = self.ALPHABETS_FULL
97 97 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
98 98 return self.passwd
99 99
100 100
101 101 class _RhodeCodeCryptoBase(object):
102 ENC_PREF = None
102 103
103 104 def hash_create(self, str_):
104 105 """
105 106 hash the string using
106 107
107 108 :param str_: password to hash
108 109 """
109 110 raise NotImplementedError
110 111
111 112 def hash_check_with_upgrade(self, password, hashed):
112 113 """
113 114 Returns tuple in which first element is boolean that states that
114 115 given password matches it's hashed version, and the second is new hash
115 116 of the password, in case this password should be migrated to new
116 117 cipher.
117 118 """
118 119 checked_hash = self.hash_check(password, hashed)
119 120 return checked_hash, None
120 121
121 122 def hash_check(self, password, hashed):
122 123 """
123 124 Checks matching password with it's hashed value.
124 125
125 126 :param password: password
126 127 :param hashed: password in hashed form
127 128 """
128 129 raise NotImplementedError
129 130
130 131 def _assert_bytes(self, value):
131 132 """
132 133 Passing in an `unicode` object can lead to hard to detect issues
133 134 if passwords contain non-ascii characters. Doing a type check
134 135 during runtime, so that such mistakes are detected early on.
135 136 """
136 137 if not isinstance(value, str):
137 138 raise TypeError(
138 139 "Bytestring required as input, got %r." % (value, ))
139 140
140 141
141 142 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
143 ENC_PREF = '$2a$10'
142 144
143 145 def hash_create(self, str_):
144 146 self._assert_bytes(str_)
145 147 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
146 148
147 149 def hash_check_with_upgrade(self, password, hashed):
148 150 """
149 151 Returns tuple in which first element is boolean that states that
150 152 given password matches it's hashed version, and the second is new hash
151 153 of the password, in case this password should be migrated to new
152 154 cipher.
153 155
154 156 This implements special upgrade logic which works like that:
155 157 - check if the given password == bcrypted hash, if yes then we
156 158 properly used password and it was already in bcrypt. Proceed
157 159 without any changes
158 160 - if bcrypt hash check is not working try with sha256. If hash compare
159 161 is ok, it means we using correct but old hashed password. indicate
160 162 hash change and proceed
161 163 """
162 164
163 165 new_hash = None
164 166
165 167 # regular pw check
166 168 password_match_bcrypt = self.hash_check(password, hashed)
167 169
168 170 # now we want to know if the password was maybe from sha256
169 171 # basically calling _RhodeCodeCryptoSha256().hash_check()
170 172 if not password_match_bcrypt:
171 173 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
172 174 new_hash = self.hash_create(password) # make new bcrypt hash
173 175 password_match_bcrypt = True
174 176
175 177 return password_match_bcrypt, new_hash
176 178
177 179 def hash_check(self, password, hashed):
178 180 """
179 181 Checks matching password with it's hashed value.
180 182
181 183 :param password: password
182 184 :param hashed: password in hashed form
183 185 """
184 186 self._assert_bytes(password)
185 187 try:
186 188 return bcrypt.hashpw(password, hashed) == hashed
187 189 except ValueError as e:
188 190 # we're having a invalid salt here probably, we should not crash
189 191 # just return with False as it would be a wrong password.
190 192 log.debug('Failed to check password hash using bcrypt %s',
191 193 safe_str(e))
192 194
193 195 return False
194 196
195 197
196 198 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
199 ENC_PREF = '_'
197 200
198 201 def hash_create(self, str_):
199 202 self._assert_bytes(str_)
200 203 return hashlib.sha256(str_).hexdigest()
201 204
202 205 def hash_check(self, password, hashed):
203 206 """
204 207 Checks matching password with it's hashed value.
205 208
206 209 :param password: password
207 210 :param hashed: password in hashed form
208 211 """
209 212 self._assert_bytes(password)
210 213 return hashlib.sha256(password).hexdigest() == hashed
211 214
212 215
213 216 class _RhodeCodeCryptoMd5(_RhodeCodeCryptoBase):
217 ENC_PREF = '_'
214 218
215 219 def hash_create(self, str_):
216 220 self._assert_bytes(str_)
217 221 return hashlib.md5(str_).hexdigest()
218 222
219 223 def hash_check(self, password, hashed):
220 224 """
221 225 Checks matching password with it's hashed value.
222 226
223 227 :param password: password
224 228 :param hashed: password in hashed form
225 229 """
226 230 self._assert_bytes(password)
227 231 return hashlib.md5(password).hexdigest() == hashed
228 232
229 233
230 234 def crypto_backend():
231 235 """
232 236 Return the matching crypto backend.
233 237
234 238 Selection is based on if we run tests or not, we pick md5 backend to run
235 239 tests faster since BCRYPT is expensive to calculate
236 240 """
237 241 if rhodecode.is_test:
238 242 RhodeCodeCrypto = _RhodeCodeCryptoMd5()
239 243 else:
240 244 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
241 245
242 246 return RhodeCodeCrypto
243 247
244 248
245 249 def get_crypt_password(password):
246 250 """
247 251 Create the hash of `password` with the active crypto backend.
248 252
249 253 :param password: The cleartext password.
250 254 :type password: unicode
251 255 """
252 256 password = safe_str(password)
253 257 return crypto_backend().hash_create(password)
254 258
255 259
256 260 def check_password(password, hashed):
257 261 """
258 262 Check if the value in `password` matches the hash in `hashed`.
259 263
260 264 :param password: The cleartext password.
261 265 :type password: unicode
262 266
263 267 :param hashed: The expected hashed version of the password.
264 268 :type hashed: The hash has to be passed in in text representation.
265 269 """
266 270 password = safe_str(password)
267 271 return crypto_backend().hash_check(password, hashed)
268 272
269 273
270 274 def generate_auth_token(data, salt=None):
271 275 """
272 276 Generates API KEY from given string
273 277 """
274 278
275 279 if salt is None:
276 280 salt = os.urandom(16)
277 281 return hashlib.sha1(safe_str(data) + salt).hexdigest()
278 282
279 283
280 284 class CookieStoreWrapper(object):
281 285
282 286 def __init__(self, cookie_store):
283 287 self.cookie_store = cookie_store
284 288
285 289 def __repr__(self):
286 290 return 'CookieStore<%s>' % (self.cookie_store)
287 291
288 292 def get(self, key, other=None):
289 293 if isinstance(self.cookie_store, dict):
290 294 return self.cookie_store.get(key, other)
291 295 elif isinstance(self.cookie_store, AuthUser):
292 296 return self.cookie_store.__dict__.get(key, other)
293 297
294 298
295 299 def _cached_perms_data(user_id, scope, user_is_admin,
296 300 user_inherit_default_permissions, explicit, algo):
297 301
298 302 permissions = PermissionCalculator(
299 303 user_id, scope, user_is_admin, user_inherit_default_permissions,
300 304 explicit, algo)
301 305 return permissions.calculate()
302 306
303 307 class PermOrigin:
304 308 ADMIN = 'superadmin'
305 309
306 310 REPO_USER = 'user:%s'
307 311 REPO_USERGROUP = 'usergroup:%s'
308 312 REPO_OWNER = 'repo.owner'
309 313 REPO_DEFAULT = 'repo.default'
310 314 REPO_PRIVATE = 'repo.private'
311 315
312 316 REPOGROUP_USER = 'user:%s'
313 317 REPOGROUP_USERGROUP = 'usergroup:%s'
314 318 REPOGROUP_OWNER = 'group.owner'
315 319 REPOGROUP_DEFAULT = 'group.default'
316 320
317 321 USERGROUP_USER = 'user:%s'
318 322 USERGROUP_USERGROUP = 'usergroup:%s'
319 323 USERGROUP_OWNER = 'usergroup.owner'
320 324 USERGROUP_DEFAULT = 'usergroup.default'
321 325
322 326
323 327 class PermOriginDict(dict):
324 328 """
325 329 A special dict used for tracking permissions along with their origins.
326 330
327 331 `__setitem__` has been overridden to expect a tuple(perm, origin)
328 332 `__getitem__` will return only the perm
329 333 `.perm_origin_stack` will return the stack of (perm, origin) set per key
330 334
331 335 >>> perms = PermOriginDict()
332 336 >>> perms['resource'] = 'read', 'default'
333 337 >>> perms['resource']
334 338 'read'
335 339 >>> perms['resource'] = 'write', 'admin'
336 340 >>> perms['resource']
337 341 'write'
338 342 >>> perms.perm_origin_stack
339 343 {'resource': [('read', 'default'), ('write', 'admin')]}
340 344 """
341 345
342 346
343 347 def __init__(self, *args, **kw):
344 348 dict.__init__(self, *args, **kw)
345 349 self.perm_origin_stack = {}
346 350
347 351 def __setitem__(self, key, (perm, origin)):
348 352 self.perm_origin_stack.setdefault(key, []).append((perm, origin))
349 353 dict.__setitem__(self, key, perm)
350 354
351 355
352 356 class PermissionCalculator(object):
353 357
354 358 def __init__(
355 359 self, user_id, scope, user_is_admin,
356 360 user_inherit_default_permissions, explicit, algo):
357 361 self.user_id = user_id
358 362 self.user_is_admin = user_is_admin
359 363 self.inherit_default_permissions = user_inherit_default_permissions
360 364 self.explicit = explicit
361 365 self.algo = algo
362 366
363 367 scope = scope or {}
364 368 self.scope_repo_id = scope.get('repo_id')
365 369 self.scope_repo_group_id = scope.get('repo_group_id')
366 370 self.scope_user_group_id = scope.get('user_group_id')
367 371
368 372 self.default_user_id = User.get_default_user(cache=True).user_id
369 373
370 374 self.permissions_repositories = PermOriginDict()
371 375 self.permissions_repository_groups = PermOriginDict()
372 376 self.permissions_user_groups = PermOriginDict()
373 377 self.permissions_global = set()
374 378
375 379 self.default_repo_perms = Permission.get_default_repo_perms(
376 380 self.default_user_id, self.scope_repo_id)
377 381 self.default_repo_groups_perms = Permission.get_default_group_perms(
378 382 self.default_user_id, self.scope_repo_group_id)
379 383 self.default_user_group_perms = \
380 384 Permission.get_default_user_group_perms(
381 385 self.default_user_id, self.scope_user_group_id)
382 386
383 387 def calculate(self):
384 388 if self.user_is_admin:
385 389 return self._admin_permissions()
386 390
387 391 self._calculate_global_default_permissions()
388 392 self._calculate_global_permissions()
389 393 self._calculate_default_permissions()
390 394 self._calculate_repository_permissions()
391 395 self._calculate_repository_group_permissions()
392 396 self._calculate_user_group_permissions()
393 397 return self._permission_structure()
394 398
395 399 def _admin_permissions(self):
396 400 """
397 401 admin user have all default rights for repositories
398 402 and groups set to admin
399 403 """
400 404 self.permissions_global.add('hg.admin')
401 405 self.permissions_global.add('hg.create.write_on_repogroup.true')
402 406
403 407 # repositories
404 408 for perm in self.default_repo_perms:
405 409 r_k = perm.UserRepoToPerm.repository.repo_name
406 410 p = 'repository.admin'
407 411 self.permissions_repositories[r_k] = p, PermOrigin.ADMIN
408 412
409 413 # repository groups
410 414 for perm in self.default_repo_groups_perms:
411 415 rg_k = perm.UserRepoGroupToPerm.group.group_name
412 416 p = 'group.admin'
413 417 self.permissions_repository_groups[rg_k] = p, PermOrigin.ADMIN
414 418
415 419 # user groups
416 420 for perm in self.default_user_group_perms:
417 421 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
418 422 p = 'usergroup.admin'
419 423 self.permissions_user_groups[u_k] = p, PermOrigin.ADMIN
420 424
421 425 return self._permission_structure()
422 426
423 427 def _calculate_global_default_permissions(self):
424 428 """
425 429 global permissions taken from the default user
426 430 """
427 431 default_global_perms = UserToPerm.query()\
428 432 .filter(UserToPerm.user_id == self.default_user_id)\
429 433 .options(joinedload(UserToPerm.permission))
430 434
431 435 for perm in default_global_perms:
432 436 self.permissions_global.add(perm.permission.permission_name)
433 437
434 438 def _calculate_global_permissions(self):
435 439 """
436 440 Set global system permissions with user permissions or permissions
437 441 taken from the user groups of the current user.
438 442
439 443 The permissions include repo creating, repo group creating, forking
440 444 etc.
441 445 """
442 446
443 447 # now we read the defined permissions and overwrite what we have set
444 448 # before those can be configured from groups or users explicitly.
445 449
446 450 # TODO: johbo: This seems to be out of sync, find out the reason
447 451 # for the comment below and update it.
448 452
449 453 # In case we want to extend this list we should be always in sync with
450 454 # User.DEFAULT_USER_PERMISSIONS definitions
451 455 _configurable = frozenset([
452 456 'hg.fork.none', 'hg.fork.repository',
453 457 'hg.create.none', 'hg.create.repository',
454 458 'hg.usergroup.create.false', 'hg.usergroup.create.true',
455 459 'hg.repogroup.create.false', 'hg.repogroup.create.true',
456 460 'hg.create.write_on_repogroup.false',
457 461 'hg.create.write_on_repogroup.true',
458 462 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
459 463 ])
460 464
461 465 # USER GROUPS comes first user group global permissions
462 466 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
463 467 .options(joinedload(UserGroupToPerm.permission))\
464 468 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
465 469 UserGroupMember.users_group_id))\
466 470 .filter(UserGroupMember.user_id == self.user_id)\
467 471 .order_by(UserGroupToPerm.users_group_id)\
468 472 .all()
469 473
470 474 # need to group here by groups since user can be in more than
471 475 # one group, so we get all groups
472 476 _explicit_grouped_perms = [
473 477 [x, list(y)] for x, y in
474 478 itertools.groupby(user_perms_from_users_groups,
475 479 lambda _x: _x.users_group)]
476 480
477 481 for gr, perms in _explicit_grouped_perms:
478 482 # since user can be in multiple groups iterate over them and
479 483 # select the lowest permissions first (more explicit)
480 484 # TODO: marcink: do this^^
481 485
482 486 # group doesn't inherit default permissions so we actually set them
483 487 if not gr.inherit_default_permissions:
484 488 # NEED TO IGNORE all previously set configurable permissions
485 489 # and replace them with explicitly set from this user
486 490 # group permissions
487 491 self.permissions_global = self.permissions_global.difference(
488 492 _configurable)
489 493 for perm in perms:
490 494 self.permissions_global.add(perm.permission.permission_name)
491 495
492 496 # user explicit global permissions
493 497 user_perms = Session().query(UserToPerm)\
494 498 .options(joinedload(UserToPerm.permission))\
495 499 .filter(UserToPerm.user_id == self.user_id).all()
496 500
497 501 if not self.inherit_default_permissions:
498 502 # NEED TO IGNORE all configurable permissions and
499 503 # replace them with explicitly set from this user permissions
500 504 self.permissions_global = self.permissions_global.difference(
501 505 _configurable)
502 506 for perm in user_perms:
503 507 self.permissions_global.add(perm.permission.permission_name)
504 508
505 509 def _calculate_default_permissions(self):
506 510 """
507 511 Set default user permissions for repositories, repository groups
508 512 taken from the default user.
509 513
510 514 Calculate inheritance of object permissions based on what we have now
511 515 in GLOBAL permissions. We check if .false is in GLOBAL since this is
512 516 explicitly set. Inherit is the opposite of .false being there.
513 517
514 518 .. note::
515 519
516 520 the syntax is little bit odd but what we need to check here is
517 521 the opposite of .false permission being in the list so even for
518 522 inconsistent state when both .true/.false is there
519 523 .false is more important
520 524
521 525 """
522 526 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
523 527 in self.permissions_global)
524 528
525 529 # defaults for repositories, taken from `default` user permissions
526 530 # on given repo
527 531 for perm in self.default_repo_perms:
528 532 r_k = perm.UserRepoToPerm.repository.repo_name
529 533 o = PermOrigin.REPO_DEFAULT
530 534 if perm.Repository.private and not (
531 535 perm.Repository.user_id == self.user_id):
532 536 # disable defaults for private repos,
533 537 p = 'repository.none'
534 538 o = PermOrigin.REPO_PRIVATE
535 539 elif perm.Repository.user_id == self.user_id:
536 540 # set admin if owner
537 541 p = 'repository.admin'
538 542 o = PermOrigin.REPO_OWNER
539 543 else:
540 544 p = perm.Permission.permission_name
541 545 # if we decide this user isn't inheriting permissions from
542 546 # default user we set him to .none so only explicit
543 547 # permissions work
544 548 if not user_inherit_object_permissions:
545 549 p = 'repository.none'
546 550 self.permissions_repositories[r_k] = p, o
547 551
548 552 # defaults for repository groups taken from `default` user permission
549 553 # on given group
550 554 for perm in self.default_repo_groups_perms:
551 555 rg_k = perm.UserRepoGroupToPerm.group.group_name
552 556 o = PermOrigin.REPOGROUP_DEFAULT
553 557 if perm.RepoGroup.user_id == self.user_id:
554 558 # set admin if owner
555 559 p = 'group.admin'
556 560 o = PermOrigin.REPOGROUP_OWNER
557 561 else:
558 562 p = perm.Permission.permission_name
559 563
560 564 # if we decide this user isn't inheriting permissions from default
561 565 # user we set him to .none so only explicit permissions work
562 566 if not user_inherit_object_permissions:
563 567 p = 'group.none'
564 568 self.permissions_repository_groups[rg_k] = p, o
565 569
566 570 # defaults for user groups taken from `default` user permission
567 571 # on given user group
568 572 for perm in self.default_user_group_perms:
569 573 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
570 574 p = perm.Permission.permission_name
571 575 o = PermOrigin.USERGROUP_DEFAULT
572 576 # if we decide this user isn't inheriting permissions from default
573 577 # user we set him to .none so only explicit permissions work
574 578 if not user_inherit_object_permissions:
575 579 p = 'usergroup.none'
576 580 self.permissions_user_groups[u_k] = p, o
577 581
578 582 def _calculate_repository_permissions(self):
579 583 """
580 584 Repository permissions for the current user.
581 585
582 586 Check if the user is part of user groups for this repository and
583 587 fill in the permission from it. `_choose_permission` decides of which
584 588 permission should be selected based on selected method.
585 589 """
586 590
587 591 # user group for repositories permissions
588 592 user_repo_perms_from_user_group = Permission\
589 593 .get_default_repo_perms_from_user_group(
590 594 self.user_id, self.scope_repo_id)
591 595
592 596 multiple_counter = collections.defaultdict(int)
593 597 for perm in user_repo_perms_from_user_group:
594 598 r_k = perm.UserGroupRepoToPerm.repository.repo_name
595 599 ug_k = perm.UserGroupRepoToPerm.users_group.users_group_name
596 600 multiple_counter[r_k] += 1
597 601 p = perm.Permission.permission_name
598 602 o = PermOrigin.REPO_USERGROUP % ug_k
599 603
600 604 if perm.Repository.user_id == self.user_id:
601 605 # set admin if owner
602 606 p = 'repository.admin'
603 607 o = PermOrigin.REPO_OWNER
604 608 else:
605 609 if multiple_counter[r_k] > 1:
606 610 cur_perm = self.permissions_repositories[r_k]
607 611 p = self._choose_permission(p, cur_perm)
608 612 self.permissions_repositories[r_k] = p, o
609 613
610 614 # user explicit permissions for repositories, overrides any specified
611 615 # by the group permission
612 616 user_repo_perms = Permission.get_default_repo_perms(
613 617 self.user_id, self.scope_repo_id)
614 618 for perm in user_repo_perms:
615 619 r_k = perm.UserRepoToPerm.repository.repo_name
616 620 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
617 621 # set admin if owner
618 622 if perm.Repository.user_id == self.user_id:
619 623 p = 'repository.admin'
620 624 o = PermOrigin.REPO_OWNER
621 625 else:
622 626 p = perm.Permission.permission_name
623 627 if not self.explicit:
624 628 cur_perm = self.permissions_repositories.get(
625 629 r_k, 'repository.none')
626 630 p = self._choose_permission(p, cur_perm)
627 631 self.permissions_repositories[r_k] = p, o
628 632
629 633 def _calculate_repository_group_permissions(self):
630 634 """
631 635 Repository group permissions for the current user.
632 636
633 637 Check if the user is part of user groups for repository groups and
634 638 fill in the permissions from it. `_choose_permmission` decides of which
635 639 permission should be selected based on selected method.
636 640 """
637 641 # user group for repo groups permissions
638 642 user_repo_group_perms_from_user_group = Permission\
639 643 .get_default_group_perms_from_user_group(
640 644 self.user_id, self.scope_repo_group_id)
641 645
642 646 multiple_counter = collections.defaultdict(int)
643 647 for perm in user_repo_group_perms_from_user_group:
644 648 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
645 649 ug_k = perm.UserGroupRepoGroupToPerm.users_group.users_group_name
646 650 o = PermOrigin.REPOGROUP_USERGROUP % ug_k
647 651 multiple_counter[g_k] += 1
648 652 p = perm.Permission.permission_name
649 653 if perm.RepoGroup.user_id == self.user_id:
650 654 # set admin if owner
651 655 p = 'group.admin'
652 656 o = PermOrigin.REPOGROUP_OWNER
653 657 else:
654 658 if multiple_counter[g_k] > 1:
655 659 cur_perm = self.permissions_repository_groups[g_k]
656 660 p = self._choose_permission(p, cur_perm)
657 661 self.permissions_repository_groups[g_k] = p, o
658 662
659 663 # user explicit permissions for repository groups
660 664 user_repo_groups_perms = Permission.get_default_group_perms(
661 665 self.user_id, self.scope_repo_group_id)
662 666 for perm in user_repo_groups_perms:
663 667 rg_k = perm.UserRepoGroupToPerm.group.group_name
664 668 u_k = perm.UserRepoGroupToPerm.user.username
665 669 o = PermOrigin.REPOGROUP_USER % u_k
666 670
667 671 if perm.RepoGroup.user_id == self.user_id:
668 672 # set admin if owner
669 673 p = 'group.admin'
670 674 o = PermOrigin.REPOGROUP_OWNER
671 675 else:
672 676 p = perm.Permission.permission_name
673 677 if not self.explicit:
674 678 cur_perm = self.permissions_repository_groups.get(
675 679 rg_k, 'group.none')
676 680 p = self._choose_permission(p, cur_perm)
677 681 self.permissions_repository_groups[rg_k] = p, o
678 682
679 683 def _calculate_user_group_permissions(self):
680 684 """
681 685 User group permissions for the current user.
682 686 """
683 687 # user group for user group permissions
684 688 user_group_from_user_group = Permission\
685 689 .get_default_user_group_perms_from_user_group(
686 690 self.user_id, self.scope_repo_group_id)
687 691
688 692 multiple_counter = collections.defaultdict(int)
689 693 for perm in user_group_from_user_group:
690 694 g_k = perm.UserGroupUserGroupToPerm\
691 695 .target_user_group.users_group_name
692 696 u_k = perm.UserGroupUserGroupToPerm\
693 697 .user_group.users_group_name
694 698 o = PermOrigin.USERGROUP_USERGROUP % u_k
695 699 multiple_counter[g_k] += 1
696 700 p = perm.Permission.permission_name
697 701 if multiple_counter[g_k] > 1:
698 702 cur_perm = self.permissions_user_groups[g_k]
699 703 p = self._choose_permission(p, cur_perm)
700 704 self.permissions_user_groups[g_k] = p, o
701 705
702 706 # user explicit permission for user groups
703 707 user_user_groups_perms = Permission.get_default_user_group_perms(
704 708 self.user_id, self.scope_user_group_id)
705 709 for perm in user_user_groups_perms:
706 710 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
707 711 u_k = perm.UserUserGroupToPerm.user.username
708 712 p = perm.Permission.permission_name
709 713 o = PermOrigin.USERGROUP_USER % u_k
710 714 if not self.explicit:
711 715 cur_perm = self.permissions_user_groups.get(
712 716 ug_k, 'usergroup.none')
713 717 p = self._choose_permission(p, cur_perm)
714 718 self.permissions_user_groups[ug_k] = p, o
715 719
716 720 def _choose_permission(self, new_perm, cur_perm):
717 721 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
718 722 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
719 723 if self.algo == 'higherwin':
720 724 if new_perm_val > cur_perm_val:
721 725 return new_perm
722 726 return cur_perm
723 727 elif self.algo == 'lowerwin':
724 728 if new_perm_val < cur_perm_val:
725 729 return new_perm
726 730 return cur_perm
727 731
728 732 def _permission_structure(self):
729 733 return {
730 734 'global': self.permissions_global,
731 735 'repositories': self.permissions_repositories,
732 736 'repositories_groups': self.permissions_repository_groups,
733 737 'user_groups': self.permissions_user_groups,
734 738 }
735 739
736 740
737 741 def allowed_auth_token_access(controller_name, whitelist=None, auth_token=None):
738 742 """
739 743 Check if given controller_name is in whitelist of auth token access
740 744 """
741 745 if not whitelist:
742 746 from rhodecode import CONFIG
743 747 whitelist = aslist(
744 748 CONFIG.get('api_access_controllers_whitelist'), sep=',')
745 749 log.debug(
746 750 'Allowed controllers for AUTH TOKEN access: %s' % (whitelist,))
747 751
748 752 auth_token_access_valid = False
749 753 for entry in whitelist:
750 754 if fnmatch.fnmatch(controller_name, entry):
751 755 auth_token_access_valid = True
752 756 break
753 757
754 758 if auth_token_access_valid:
755 759 log.debug('controller:%s matches entry in whitelist'
756 760 % (controller_name,))
757 761 else:
758 762 msg = ('controller: %s does *NOT* match any entry in whitelist'
759 763 % (controller_name,))
760 764 if auth_token:
761 765 # if we use auth token key and don't have access it's a warning
762 766 log.warning(msg)
763 767 else:
764 768 log.debug(msg)
765 769
766 770 return auth_token_access_valid
767 771
768 772
769 773 class AuthUser(object):
770 774 """
771 775 A simple object that handles all attributes of user in RhodeCode
772 776
773 777 It does lookup based on API key,given user, or user present in session
774 778 Then it fills all required information for such user. It also checks if
775 779 anonymous access is enabled and if so, it returns default user as logged in
776 780 """
777 781 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
778 782
779 783 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
780 784
781 785 self.user_id = user_id
782 786 self._api_key = api_key
783 787
784 788 self.api_key = None
785 789 self.feed_token = ''
786 790 self.username = username
787 791 self.ip_addr = ip_addr
788 792 self.name = ''
789 793 self.lastname = ''
790 794 self.email = ''
791 795 self.is_authenticated = False
792 796 self.admin = False
793 797 self.inherit_default_permissions = False
794 798 self.password = ''
795 799
796 800 self.anonymous_user = None # propagated on propagate_data
797 801 self.propagate_data()
798 802 self._instance = None
799 803 self._permissions_scoped_cache = {} # used to bind scoped calculation
800 804
801 805 @LazyProperty
802 806 def permissions(self):
803 807 return self.get_perms(user=self, cache=False)
804 808
805 809 def permissions_with_scope(self, scope):
806 810 """
807 811 Call the get_perms function with scoped data. The scope in that function
808 812 narrows the SQL calls to the given ID of objects resulting in fetching
809 813 Just particular permission we want to obtain. If scope is an empty dict
810 814 then it basically narrows the scope to GLOBAL permissions only.
811 815
812 816 :param scope: dict
813 817 """
814 818 if 'repo_name' in scope:
815 819 obj = Repository.get_by_repo_name(scope['repo_name'])
816 820 if obj:
817 821 scope['repo_id'] = obj.repo_id
818 822 _scope = {
819 823 'repo_id': -1,
820 824 'user_group_id': -1,
821 825 'repo_group_id': -1,
822 826 }
823 827 _scope.update(scope)
824 828 cache_key = "_".join(map(safe_str, reduce(lambda a, b: a+b,
825 829 _scope.items())))
826 830 if cache_key not in self._permissions_scoped_cache:
827 831 # store in cache to mimic how the @LazyProperty works,
828 832 # the difference here is that we use the unique key calculated
829 833 # from params and values
830 834 res = self.get_perms(user=self, cache=False, scope=_scope)
831 835 self._permissions_scoped_cache[cache_key] = res
832 836 return self._permissions_scoped_cache[cache_key]
833 837
834 @property
835 def auth_tokens(self):
836 return self.get_auth_tokens()
837
838 838 def get_instance(self):
839 839 return User.get(self.user_id)
840 840
841 841 def update_lastactivity(self):
842 842 if self.user_id:
843 843 User.get(self.user_id).update_lastactivity()
844 844
845 845 def propagate_data(self):
846 846 """
847 847 Fills in user data and propagates values to this instance. Maps fetched
848 848 user attributes to this class instance attributes
849 849 """
850 850 log.debug('starting data propagation for new potential AuthUser')
851 851 user_model = UserModel()
852 852 anon_user = self.anonymous_user = User.get_default_user(cache=True)
853 853 is_user_loaded = False
854 854
855 855 # lookup by userid
856 856 if self.user_id is not None and self.user_id != anon_user.user_id:
857 857 log.debug('Trying Auth User lookup by USER ID: `%s`' % self.user_id)
858 858 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
859 859
860 860 # try go get user by api key
861 861 elif self._api_key and self._api_key != anon_user.api_key:
862 862 log.debug('Trying Auth User lookup by API KEY: `%s`' % self._api_key)
863 863 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
864 864
865 865 # lookup by username
866 866 elif self.username:
867 867 log.debug('Trying Auth User lookup by USER NAME: `%s`' % self.username)
868 868 is_user_loaded = user_model.fill_data(self, username=self.username)
869 869 else:
870 870 log.debug('No data in %s that could been used to log in' % self)
871 871
872 872 if not is_user_loaded:
873 873 log.debug('Failed to load user. Fallback to default user')
874 874 # if we cannot authenticate user try anonymous
875 875 if anon_user.active:
876 876 user_model.fill_data(self, user_id=anon_user.user_id)
877 877 # then we set this user is logged in
878 878 self.is_authenticated = True
879 879 else:
880 880 # in case of disabled anonymous user we reset some of the
881 881 # parameters so such user is "corrupted", skipping the fill_data
882 882 for attr in ['user_id', 'username', 'admin', 'active']:
883 883 setattr(self, attr, None)
884 884 self.is_authenticated = False
885 885
886 886 if not self.username:
887 887 self.username = 'None'
888 888
889 889 log.debug('Auth User is now %s' % self)
890 890
891 891 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
892 892 cache=False):
893 893 """
894 894 Fills user permission attribute with permissions taken from database
895 895 works for permissions given for repositories, and for permissions that
896 896 are granted to groups
897 897
898 898 :param user: instance of User object from database
899 899 :param explicit: In case there are permissions both for user and a group
900 900 that user is part of, explicit flag will defiine if user will
901 901 explicitly override permissions from group, if it's False it will
902 902 make decision based on the algo
903 903 :param algo: algorithm to decide what permission should be choose if
904 904 it's multiple defined, eg user in two different groups. It also
905 905 decides if explicit flag is turned off how to specify the permission
906 906 for case when user is in a group + have defined separate permission
907 907 """
908 908 user_id = user.user_id
909 909 user_is_admin = user.is_admin
910 910
911 911 # inheritance of global permissions like create repo/fork repo etc
912 912 user_inherit_default_permissions = user.inherit_default_permissions
913 913
914 914 log.debug('Computing PERMISSION tree for scope %s' % (scope, ))
915 915 compute = caches.conditional_cache(
916 916 'short_term', 'cache_desc',
917 917 condition=cache, func=_cached_perms_data)
918 918 result = compute(user_id, scope, user_is_admin,
919 919 user_inherit_default_permissions, explicit, algo)
920 920
921 921 result_repr = []
922 922 for k in result:
923 923 result_repr.append((k, len(result[k])))
924 924
925 925 log.debug('PERMISSION tree computed %s' % (result_repr,))
926 926 return result
927 927
928 def get_auth_tokens(self):
929 auth_tokens = [self.api_key]
930 for api_key in UserApiKeys.query()\
931 .filter(UserApiKeys.user_id == self.user_id)\
932 .filter(or_(UserApiKeys.expires == -1,
933 UserApiKeys.expires >= time.time())).all():
934 auth_tokens.append(api_key.api_key)
935
936 return auth_tokens
937
938 928 @property
939 929 def is_default(self):
940 930 return self.username == User.DEFAULT_USER
941 931
942 932 @property
943 933 def is_admin(self):
944 934 return self.admin
945 935
946 936 @property
947 937 def is_user_object(self):
948 938 return self.user_id is not None
949 939
950 940 @property
951 941 def repositories_admin(self):
952 942 """
953 943 Returns list of repositories you're an admin of
954 944 """
955 945 return [x[0] for x in self.permissions['repositories'].iteritems()
956 946 if x[1] == 'repository.admin']
957 947
958 948 @property
959 949 def repository_groups_admin(self):
960 950 """
961 951 Returns list of repository groups you're an admin of
962 952 """
963 953 return [x[0]
964 954 for x in self.permissions['repositories_groups'].iteritems()
965 955 if x[1] == 'group.admin']
966 956
967 957 @property
968 958 def user_groups_admin(self):
969 959 """
970 960 Returns list of user groups you're an admin of
971 961 """
972 962 return [x[0] for x in self.permissions['user_groups'].iteritems()
973 963 if x[1] == 'usergroup.admin']
974 964
975 965 @property
976 966 def ip_allowed(self):
977 967 """
978 968 Checks if ip_addr used in constructor is allowed from defined list of
979 969 allowed ip_addresses for user
980 970
981 971 :returns: boolean, True if ip is in allowed ip range
982 972 """
983 973 # check IP
984 974 inherit = self.inherit_default_permissions
985 975 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
986 976 inherit_from_default=inherit)
987 977 @property
988 978 def personal_repo_group(self):
989 979 return RepoGroup.get_user_personal_repo_group(self.user_id)
990 980
991 981 @classmethod
992 982 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
993 983 allowed_ips = AuthUser.get_allowed_ips(
994 984 user_id, cache=True, inherit_from_default=inherit_from_default)
995 985 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
996 986 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
997 987 return True
998 988 else:
999 989 log.info('Access for IP:%s forbidden, '
1000 990 'not in %s' % (ip_addr, allowed_ips))
1001 991 return False
1002 992
1003 993 def __repr__(self):
1004 994 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1005 995 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1006 996
1007 997 def set_authenticated(self, authenticated=True):
1008 998 if self.user_id != self.anonymous_user.user_id:
1009 999 self.is_authenticated = authenticated
1010 1000
1011 1001 def get_cookie_store(self):
1012 1002 return {
1013 1003 'username': self.username,
1014 1004 'password': md5(self.password),
1015 1005 'user_id': self.user_id,
1016 1006 'is_authenticated': self.is_authenticated
1017 1007 }
1018 1008
1019 1009 @classmethod
1020 1010 def from_cookie_store(cls, cookie_store):
1021 1011 """
1022 1012 Creates AuthUser from a cookie store
1023 1013
1024 1014 :param cls:
1025 1015 :param cookie_store:
1026 1016 """
1027 1017 user_id = cookie_store.get('user_id')
1028 1018 username = cookie_store.get('username')
1029 1019 api_key = cookie_store.get('api_key')
1030 1020 return AuthUser(user_id, api_key, username)
1031 1021
1032 1022 @classmethod
1033 1023 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1034 1024 _set = set()
1035 1025
1036 1026 if inherit_from_default:
1037 1027 default_ips = UserIpMap.query().filter(
1038 1028 UserIpMap.user == User.get_default_user(cache=True))
1039 1029 if cache:
1040 1030 default_ips = default_ips.options(FromCache("sql_cache_short",
1041 1031 "get_user_ips_default"))
1042 1032
1043 1033 # populate from default user
1044 1034 for ip in default_ips:
1045 1035 try:
1046 1036 _set.add(ip.ip_addr)
1047 1037 except ObjectDeletedError:
1048 1038 # since we use heavy caching sometimes it happens that
1049 1039 # we get deleted objects here, we just skip them
1050 1040 pass
1051 1041
1052 1042 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1053 1043 if cache:
1054 1044 user_ips = user_ips.options(FromCache("sql_cache_short",
1055 1045 "get_user_ips_%s" % user_id))
1056 1046
1057 1047 for ip in user_ips:
1058 1048 try:
1059 1049 _set.add(ip.ip_addr)
1060 1050 except ObjectDeletedError:
1061 1051 # since we use heavy caching sometimes it happens that we get
1062 1052 # deleted objects here, we just skip them
1063 1053 pass
1064 1054 return _set or set(['0.0.0.0/0', '::/0'])
1065 1055
1066 1056
1067 1057 def set_available_permissions(config):
1068 1058 """
1069 1059 This function will propagate pylons globals with all available defined
1070 1060 permission given in db. We don't want to check each time from db for new
1071 1061 permissions since adding a new permission also requires application restart
1072 1062 ie. to decorate new views with the newly created permission
1073 1063
1074 1064 :param config: current pylons config instance
1075 1065
1076 1066 """
1077 1067 log.info('getting information about all available permissions')
1078 1068 try:
1079 1069 sa = meta.Session
1080 1070 all_perms = sa.query(Permission).all()
1081 1071 config['available_permissions'] = [x.permission_name for x in all_perms]
1082 1072 except Exception:
1083 1073 log.error(traceback.format_exc())
1084 1074 finally:
1085 1075 meta.Session.remove()
1086 1076
1087 1077
1088 1078 def get_csrf_token(session=None, force_new=False, save_if_missing=True):
1089 1079 """
1090 1080 Return the current authentication token, creating one if one doesn't
1091 1081 already exist and the save_if_missing flag is present.
1092 1082
1093 1083 :param session: pass in the pylons session, else we use the global ones
1094 1084 :param force_new: force to re-generate the token and store it in session
1095 1085 :param save_if_missing: save the newly generated token if it's missing in
1096 1086 session
1097 1087 """
1098 1088 if not session:
1099 1089 from pylons import session
1100 1090
1101 1091 if (csrf_token_key not in session and save_if_missing) or force_new:
1102 1092 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1103 1093 session[csrf_token_key] = token
1104 1094 if hasattr(session, 'save'):
1105 1095 session.save()
1106 1096 return session.get(csrf_token_key)
1107 1097
1108 1098
1109 1099 # CHECK DECORATORS
1110 1100 class CSRFRequired(object):
1111 1101 """
1112 1102 Decorator for authenticating a form
1113 1103
1114 1104 This decorator uses an authorization token stored in the client's
1115 1105 session for prevention of certain Cross-site request forgery (CSRF)
1116 1106 attacks (See
1117 1107 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1118 1108 information).
1119 1109
1120 1110 For use with the ``webhelpers.secure_form`` helper functions.
1121 1111
1122 1112 """
1123 1113 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1124 1114 except_methods=None):
1125 1115 self.token = token
1126 1116 self.header = header
1127 1117 self.except_methods = except_methods or []
1128 1118
1129 1119 def __call__(self, func):
1130 1120 return get_cython_compat_decorator(self.__wrapper, func)
1131 1121
1132 1122 def _get_csrf(self, _request):
1133 1123 return _request.POST.get(self.token, _request.headers.get(self.header))
1134 1124
1135 1125 def check_csrf(self, _request, cur_token):
1136 1126 supplied_token = self._get_csrf(_request)
1137 1127 return supplied_token and supplied_token == cur_token
1138 1128
1139 1129 def __wrapper(self, func, *fargs, **fkwargs):
1140 1130 if request.method in self.except_methods:
1141 1131 return func(*fargs, **fkwargs)
1142 1132
1143 1133 cur_token = get_csrf_token(save_if_missing=False)
1144 1134 if self.check_csrf(request, cur_token):
1145 1135 if request.POST.get(self.token):
1146 1136 del request.POST[self.token]
1147 1137 return func(*fargs, **fkwargs)
1148 1138 else:
1149 1139 reason = 'token-missing'
1150 1140 supplied_token = self._get_csrf(request)
1151 1141 if supplied_token and cur_token != supplied_token:
1152 1142 reason = 'token-mismatch [%s:%s]' % (cur_token or ''[:6],
1153 1143 supplied_token or ''[:6])
1154 1144
1155 1145 csrf_message = \
1156 1146 ("Cross-site request forgery detected, request denied. See "
1157 1147 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1158 1148 "more information.")
1159 1149 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1160 1150 'REMOTE_ADDR:%s, HEADERS:%s' % (
1161 1151 request, reason, request.remote_addr, request.headers))
1162 1152
1163 1153 raise HTTPForbidden(explanation=csrf_message)
1164 1154
1165 1155
1166 1156 class LoginRequired(object):
1167 1157 """
1168 1158 Must be logged in to execute this function else
1169 1159 redirect to login page
1170 1160
1171 1161 :param api_access: if enabled this checks only for valid auth token
1172 1162 and grants access based on valid token
1173 1163 """
1174 def __init__(self, auth_token_access=False):
1164 def __init__(self, auth_token_access=None):
1175 1165 self.auth_token_access = auth_token_access
1176 1166
1177 1167 def __call__(self, func):
1178 1168 return get_cython_compat_decorator(self.__wrapper, func)
1179 1169
1180 1170 def __wrapper(self, func, *fargs, **fkwargs):
1181 1171 from rhodecode.lib import helpers as h
1182 1172 cls = fargs[0]
1183 1173 user = cls._rhodecode_user
1184 1174 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1185 1175 log.debug('Starting login restriction checks for user: %s' % (user,))
1186 1176 # check if our IP is allowed
1187 1177 ip_access_valid = True
1188 1178 if not user.ip_allowed:
1189 1179 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1190 1180 category='warning')
1191 1181 ip_access_valid = False
1192 1182
1193 1183 # check if we used an APIKEY and it's a valid one
1194 # defined whitelist of controllers which API access will be enabled
1184 # defined white-list of controllers which API access will be enabled
1195 1185 _auth_token = request.GET.get(
1196 1186 'auth_token', '') or request.GET.get('api_key', '')
1197 1187 auth_token_access_valid = allowed_auth_token_access(
1198 1188 loc, auth_token=_auth_token)
1199 1189
1200 1190 # explicit controller is enabled or API is in our whitelist
1201 1191 if self.auth_token_access or auth_token_access_valid:
1202 1192 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1193 db_user = user.get_instance()
1203 1194
1204 if _auth_token and _auth_token in user.auth_tokens:
1195 if db_user:
1196 if self.auth_token_access:
1197 roles = self.auth_token_access
1198 else:
1199 roles = [UserApiKeys.ROLE_HTTP]
1200 token_match = db_user.authenticate_by_token(
1201 _auth_token, roles=roles, include_builtin_token=True)
1202 else:
1203 log.debug('Unable to fetch db instance for auth user: %s', user)
1204 token_match = False
1205
1206 if _auth_token and token_match:
1205 1207 auth_token_access_valid = True
1206 1208 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1207 1209 else:
1208 1210 auth_token_access_valid = False
1209 1211 if not _auth_token:
1210 1212 log.debug("AUTH TOKEN *NOT* present in request")
1211 1213 else:
1212 1214 log.warning(
1213 1215 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1214 1216
1215 1217 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1216 1218 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1217 1219 else 'AUTH_TOKEN_AUTH'
1218 1220
1219 1221 if ip_access_valid and (
1220 1222 user.is_authenticated or auth_token_access_valid):
1221 1223 log.info(
1222 1224 'user %s authenticating with:%s IS authenticated on func %s'
1223 1225 % (user, reason, loc))
1224 1226
1225 1227 # update user data to check last activity
1226 1228 user.update_lastactivity()
1227 1229 Session().commit()
1228 1230 return func(*fargs, **fkwargs)
1229 1231 else:
1230 1232 log.warning(
1231 1233 'user %s authenticating with:%s NOT authenticated on '
1232 1234 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1233 1235 % (user, reason, loc, ip_access_valid,
1234 1236 auth_token_access_valid))
1235 1237 # we preserve the get PARAM
1236 1238 came_from = request.path_qs
1237 1239
1238 1240 log.debug('redirecting to login page with %s' % (came_from,))
1239 1241 return redirect(
1240 1242 h.route_path('login', _query={'came_from': came_from}))
1241 1243
1242 1244
1243 1245 class NotAnonymous(object):
1244 1246 """
1245 1247 Must be logged in to execute this function else
1246 1248 redirect to login page"""
1247 1249
1248 1250 def __call__(self, func):
1249 1251 return get_cython_compat_decorator(self.__wrapper, func)
1250 1252
1251 1253 def __wrapper(self, func, *fargs, **fkwargs):
1252 1254 cls = fargs[0]
1253 1255 self.user = cls._rhodecode_user
1254 1256
1255 1257 log.debug('Checking if user is not anonymous @%s' % cls)
1256 1258
1257 1259 anonymous = self.user.username == User.DEFAULT_USER
1258 1260
1259 1261 if anonymous:
1260 1262 came_from = request.path_qs
1261 1263
1262 1264 import rhodecode.lib.helpers as h
1263 1265 h.flash(_('You need to be a registered user to '
1264 1266 'perform this action'),
1265 1267 category='warning')
1266 1268 return redirect(
1267 1269 h.route_path('login', _query={'came_from': came_from}))
1268 1270 else:
1269 1271 return func(*fargs, **fkwargs)
1270 1272
1271 1273
1272 1274 class XHRRequired(object):
1273 1275 def __call__(self, func):
1274 1276 return get_cython_compat_decorator(self.__wrapper, func)
1275 1277
1276 1278 def __wrapper(self, func, *fargs, **fkwargs):
1277 1279 log.debug('Checking if request is XMLHttpRequest (XHR)')
1278 1280 xhr_message = 'This is not a valid XMLHttpRequest (XHR) request'
1279 1281 if not request.is_xhr:
1280 1282 abort(400, detail=xhr_message)
1281 1283
1282 1284 return func(*fargs, **fkwargs)
1283 1285
1284 1286
1285 1287 class HasAcceptedRepoType(object):
1286 1288 """
1287 1289 Check if requested repo is within given repo type aliases
1288 1290
1289 1291 TODO: anderson: not sure where to put this decorator
1290 1292 """
1291 1293
1292 1294 def __init__(self, *repo_type_list):
1293 1295 self.repo_type_list = set(repo_type_list)
1294 1296
1295 1297 def __call__(self, func):
1296 1298 return get_cython_compat_decorator(self.__wrapper, func)
1297 1299
1298 1300 def __wrapper(self, func, *fargs, **fkwargs):
1299 1301 cls = fargs[0]
1300 1302 rhodecode_repo = cls.rhodecode_repo
1301 1303
1302 1304 log.debug('%s checking repo type for %s in %s',
1303 1305 self.__class__.__name__,
1304 1306 rhodecode_repo.alias, self.repo_type_list)
1305 1307
1306 1308 if rhodecode_repo.alias in self.repo_type_list:
1307 1309 return func(*fargs, **fkwargs)
1308 1310 else:
1309 1311 import rhodecode.lib.helpers as h
1310 1312 h.flash(h.literal(
1311 1313 _('Action not supported for %s.' % rhodecode_repo.alias)),
1312 1314 category='warning')
1313 1315 return redirect(
1314 1316 url('summary_home', repo_name=cls.rhodecode_db_repo.repo_name))
1315 1317
1316 1318
1317 1319 class PermsDecorator(object):
1318 1320 """
1319 1321 Base class for controller decorators, we extract the current user from
1320 1322 the class itself, which has it stored in base controllers
1321 1323 """
1322 1324
1323 1325 def __init__(self, *required_perms):
1324 1326 self.required_perms = set(required_perms)
1325 1327
1326 1328 def __call__(self, func):
1327 1329 return get_cython_compat_decorator(self.__wrapper, func)
1328 1330
1329 1331 def __wrapper(self, func, *fargs, **fkwargs):
1330 1332 cls = fargs[0]
1331 1333 _user = cls._rhodecode_user
1332 1334
1333 1335 log.debug('checking %s permissions %s for %s %s',
1334 1336 self.__class__.__name__, self.required_perms, cls, _user)
1335 1337
1336 1338 if self.check_permissions(_user):
1337 1339 log.debug('Permission granted for %s %s', cls, _user)
1338 1340 return func(*fargs, **fkwargs)
1339 1341
1340 1342 else:
1341 1343 log.debug('Permission denied for %s %s', cls, _user)
1342 1344 anonymous = _user.username == User.DEFAULT_USER
1343 1345
1344 1346 if anonymous:
1345 1347 came_from = request.path_qs
1346 1348
1347 1349 import rhodecode.lib.helpers as h
1348 1350 h.flash(_('You need to be signed in to view this page'),
1349 1351 category='warning')
1350 1352 return redirect(
1351 1353 h.route_path('login', _query={'came_from': came_from}))
1352 1354
1353 1355 else:
1354 1356 # redirect with forbidden ret code
1355 1357 return abort(403)
1356 1358
1357 1359 def check_permissions(self, user):
1358 1360 """Dummy function for overriding"""
1359 1361 raise NotImplementedError(
1360 1362 'You have to write this function in child class')
1361 1363
1362 1364
1363 1365 class HasPermissionAllDecorator(PermsDecorator):
1364 1366 """
1365 1367 Checks for access permission for all given predicates. All of them
1366 1368 have to be meet in order to fulfill the request
1367 1369 """
1368 1370
1369 1371 def check_permissions(self, user):
1370 1372 perms = user.permissions_with_scope({})
1371 1373 if self.required_perms.issubset(perms['global']):
1372 1374 return True
1373 1375 return False
1374 1376
1375 1377
1376 1378 class HasPermissionAnyDecorator(PermsDecorator):
1377 1379 """
1378 1380 Checks for access permission for any of given predicates. In order to
1379 1381 fulfill the request any of predicates must be meet
1380 1382 """
1381 1383
1382 1384 def check_permissions(self, user):
1383 1385 perms = user.permissions_with_scope({})
1384 1386 if self.required_perms.intersection(perms['global']):
1385 1387 return True
1386 1388 return False
1387 1389
1388 1390
1389 1391 class HasRepoPermissionAllDecorator(PermsDecorator):
1390 1392 """
1391 1393 Checks for access permission for all given predicates for specific
1392 1394 repository. All of them have to be meet in order to fulfill the request
1393 1395 """
1394 1396
1395 1397 def check_permissions(self, user):
1396 1398 perms = user.permissions
1397 1399 repo_name = get_repo_slug(request)
1398 1400 try:
1399 1401 user_perms = set([perms['repositories'][repo_name]])
1400 1402 except KeyError:
1401 1403 return False
1402 1404 if self.required_perms.issubset(user_perms):
1403 1405 return True
1404 1406 return False
1405 1407
1406 1408
1407 1409 class HasRepoPermissionAnyDecorator(PermsDecorator):
1408 1410 """
1409 1411 Checks for access permission for any of given predicates for specific
1410 1412 repository. In order to fulfill the request any of predicates must be meet
1411 1413 """
1412 1414
1413 1415 def check_permissions(self, user):
1414 1416 perms = user.permissions
1415 1417 repo_name = get_repo_slug(request)
1416 1418 try:
1417 1419 user_perms = set([perms['repositories'][repo_name]])
1418 1420 except KeyError:
1419 1421 return False
1420 1422
1421 1423 if self.required_perms.intersection(user_perms):
1422 1424 return True
1423 1425 return False
1424 1426
1425 1427
1426 1428 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1427 1429 """
1428 1430 Checks for access permission for all given predicates for specific
1429 1431 repository group. All of them have to be meet in order to
1430 1432 fulfill the request
1431 1433 """
1432 1434
1433 1435 def check_permissions(self, user):
1434 1436 perms = user.permissions
1435 1437 group_name = get_repo_group_slug(request)
1436 1438 try:
1437 1439 user_perms = set([perms['repositories_groups'][group_name]])
1438 1440 except KeyError:
1439 1441 return False
1440 1442
1441 1443 if self.required_perms.issubset(user_perms):
1442 1444 return True
1443 1445 return False
1444 1446
1445 1447
1446 1448 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1447 1449 """
1448 1450 Checks for access permission for any of given predicates for specific
1449 1451 repository group. In order to fulfill the request any
1450 1452 of predicates must be met
1451 1453 """
1452 1454
1453 1455 def check_permissions(self, user):
1454 1456 perms = user.permissions
1455 1457 group_name = get_repo_group_slug(request)
1456 1458 try:
1457 1459 user_perms = set([perms['repositories_groups'][group_name]])
1458 1460 except KeyError:
1459 1461 return False
1460 1462
1461 1463 if self.required_perms.intersection(user_perms):
1462 1464 return True
1463 1465 return False
1464 1466
1465 1467
1466 1468 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1467 1469 """
1468 1470 Checks for access permission for all given predicates for specific
1469 1471 user group. All of them have to be meet in order to fulfill the request
1470 1472 """
1471 1473
1472 1474 def check_permissions(self, user):
1473 1475 perms = user.permissions
1474 1476 group_name = get_user_group_slug(request)
1475 1477 try:
1476 1478 user_perms = set([perms['user_groups'][group_name]])
1477 1479 except KeyError:
1478 1480 return False
1479 1481
1480 1482 if self.required_perms.issubset(user_perms):
1481 1483 return True
1482 1484 return False
1483 1485
1484 1486
1485 1487 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1486 1488 """
1487 1489 Checks for access permission for any of given predicates for specific
1488 1490 user group. In order to fulfill the request any of predicates must be meet
1489 1491 """
1490 1492
1491 1493 def check_permissions(self, user):
1492 1494 perms = user.permissions
1493 1495 group_name = get_user_group_slug(request)
1494 1496 try:
1495 1497 user_perms = set([perms['user_groups'][group_name]])
1496 1498 except KeyError:
1497 1499 return False
1498 1500
1499 1501 if self.required_perms.intersection(user_perms):
1500 1502 return True
1501 1503 return False
1502 1504
1503 1505
1504 1506 # CHECK FUNCTIONS
1505 1507 class PermsFunction(object):
1506 1508 """Base function for other check functions"""
1507 1509
1508 1510 def __init__(self, *perms):
1509 1511 self.required_perms = set(perms)
1510 1512 self.repo_name = None
1511 1513 self.repo_group_name = None
1512 1514 self.user_group_name = None
1513 1515
1514 1516 def __bool__(self):
1515 1517 frame = inspect.currentframe()
1516 1518 stack_trace = traceback.format_stack(frame)
1517 1519 log.error('Checking bool value on a class instance of perm '
1518 1520 'function is not allowed: %s' % ''.join(stack_trace))
1519 1521 # rather than throwing errors, here we always return False so if by
1520 1522 # accident someone checks truth for just an instance it will always end
1521 1523 # up in returning False
1522 1524 return False
1523 1525 __nonzero__ = __bool__
1524 1526
1525 1527 def __call__(self, check_location='', user=None):
1526 1528 if not user:
1527 1529 log.debug('Using user attribute from global request')
1528 1530 # TODO: remove this someday,put as user as attribute here
1529 1531 user = request.user
1530 1532
1531 1533 # init auth user if not already given
1532 1534 if not isinstance(user, AuthUser):
1533 1535 log.debug('Wrapping user %s into AuthUser', user)
1534 1536 user = AuthUser(user.user_id)
1535 1537
1536 1538 cls_name = self.__class__.__name__
1537 1539 check_scope = self._get_check_scope(cls_name)
1538 1540 check_location = check_location or 'unspecified location'
1539 1541
1540 1542 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1541 1543 self.required_perms, user, check_scope, check_location)
1542 1544 if not user:
1543 1545 log.warning('Empty user given for permission check')
1544 1546 return False
1545 1547
1546 1548 if self.check_permissions(user):
1547 1549 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1548 1550 check_scope, user, check_location)
1549 1551 return True
1550 1552
1551 1553 else:
1552 1554 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1553 1555 check_scope, user, check_location)
1554 1556 return False
1555 1557
1556 1558 def _get_check_scope(self, cls_name):
1557 1559 return {
1558 1560 'HasPermissionAll': 'GLOBAL',
1559 1561 'HasPermissionAny': 'GLOBAL',
1560 1562 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1561 1563 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1562 1564 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1563 1565 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1564 1566 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1565 1567 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1566 1568 }.get(cls_name, '?:%s' % cls_name)
1567 1569
1568 1570 def check_permissions(self, user):
1569 1571 """Dummy function for overriding"""
1570 1572 raise Exception('You have to write this function in child class')
1571 1573
1572 1574
1573 1575 class HasPermissionAll(PermsFunction):
1574 1576 def check_permissions(self, user):
1575 1577 perms = user.permissions_with_scope({})
1576 1578 if self.required_perms.issubset(perms.get('global')):
1577 1579 return True
1578 1580 return False
1579 1581
1580 1582
1581 1583 class HasPermissionAny(PermsFunction):
1582 1584 def check_permissions(self, user):
1583 1585 perms = user.permissions_with_scope({})
1584 1586 if self.required_perms.intersection(perms.get('global')):
1585 1587 return True
1586 1588 return False
1587 1589
1588 1590
1589 1591 class HasRepoPermissionAll(PermsFunction):
1590 1592 def __call__(self, repo_name=None, check_location='', user=None):
1591 1593 self.repo_name = repo_name
1592 1594 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1593 1595
1594 1596 def check_permissions(self, user):
1595 1597 if not self.repo_name:
1596 1598 self.repo_name = get_repo_slug(request)
1597 1599
1598 1600 perms = user.permissions
1599 1601 try:
1600 1602 user_perms = set([perms['repositories'][self.repo_name]])
1601 1603 except KeyError:
1602 1604 return False
1603 1605 if self.required_perms.issubset(user_perms):
1604 1606 return True
1605 1607 return False
1606 1608
1607 1609
1608 1610 class HasRepoPermissionAny(PermsFunction):
1609 1611 def __call__(self, repo_name=None, check_location='', user=None):
1610 1612 self.repo_name = repo_name
1611 1613 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1612 1614
1613 1615 def check_permissions(self, user):
1614 1616 if not self.repo_name:
1615 1617 self.repo_name = get_repo_slug(request)
1616 1618
1617 1619 perms = user.permissions
1618 1620 try:
1619 1621 user_perms = set([perms['repositories'][self.repo_name]])
1620 1622 except KeyError:
1621 1623 return False
1622 1624 if self.required_perms.intersection(user_perms):
1623 1625 return True
1624 1626 return False
1625 1627
1626 1628
1627 1629 class HasRepoGroupPermissionAny(PermsFunction):
1628 1630 def __call__(self, group_name=None, check_location='', user=None):
1629 1631 self.repo_group_name = group_name
1630 1632 return super(HasRepoGroupPermissionAny, self).__call__(
1631 1633 check_location, user)
1632 1634
1633 1635 def check_permissions(self, user):
1634 1636 perms = user.permissions
1635 1637 try:
1636 1638 user_perms = set(
1637 1639 [perms['repositories_groups'][self.repo_group_name]])
1638 1640 except KeyError:
1639 1641 return False
1640 1642 if self.required_perms.intersection(user_perms):
1641 1643 return True
1642 1644 return False
1643 1645
1644 1646
1645 1647 class HasRepoGroupPermissionAll(PermsFunction):
1646 1648 def __call__(self, group_name=None, check_location='', user=None):
1647 1649 self.repo_group_name = group_name
1648 1650 return super(HasRepoGroupPermissionAll, self).__call__(
1649 1651 check_location, user)
1650 1652
1651 1653 def check_permissions(self, user):
1652 1654 perms = user.permissions
1653 1655 try:
1654 1656 user_perms = set(
1655 1657 [perms['repositories_groups'][self.repo_group_name]])
1656 1658 except KeyError:
1657 1659 return False
1658 1660 if self.required_perms.issubset(user_perms):
1659 1661 return True
1660 1662 return False
1661 1663
1662 1664
1663 1665 class HasUserGroupPermissionAny(PermsFunction):
1664 1666 def __call__(self, user_group_name=None, check_location='', user=None):
1665 1667 self.user_group_name = user_group_name
1666 1668 return super(HasUserGroupPermissionAny, self).__call__(
1667 1669 check_location, user)
1668 1670
1669 1671 def check_permissions(self, user):
1670 1672 perms = user.permissions
1671 1673 try:
1672 1674 user_perms = set([perms['user_groups'][self.user_group_name]])
1673 1675 except KeyError:
1674 1676 return False
1675 1677 if self.required_perms.intersection(user_perms):
1676 1678 return True
1677 1679 return False
1678 1680
1679 1681
1680 1682 class HasUserGroupPermissionAll(PermsFunction):
1681 1683 def __call__(self, user_group_name=None, check_location='', user=None):
1682 1684 self.user_group_name = user_group_name
1683 1685 return super(HasUserGroupPermissionAll, self).__call__(
1684 1686 check_location, user)
1685 1687
1686 1688 def check_permissions(self, user):
1687 1689 perms = user.permissions
1688 1690 try:
1689 1691 user_perms = set([perms['user_groups'][self.user_group_name]])
1690 1692 except KeyError:
1691 1693 return False
1692 1694 if self.required_perms.issubset(user_perms):
1693 1695 return True
1694 1696 return False
1695 1697
1696 1698
1697 1699 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1698 1700 class HasPermissionAnyMiddleware(object):
1699 1701 def __init__(self, *perms):
1700 1702 self.required_perms = set(perms)
1701 1703
1702 1704 def __call__(self, user, repo_name):
1703 1705 # repo_name MUST be unicode, since we handle keys in permission
1704 1706 # dict by unicode
1705 1707 repo_name = safe_unicode(repo_name)
1706 1708 user = AuthUser(user.user_id)
1707 1709 log.debug(
1708 1710 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
1709 1711 self.required_perms, user, repo_name)
1710 1712
1711 1713 if self.check_permissions(user, repo_name):
1712 1714 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
1713 1715 repo_name, user, 'PermissionMiddleware')
1714 1716 return True
1715 1717
1716 1718 else:
1717 1719 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
1718 1720 repo_name, user, 'PermissionMiddleware')
1719 1721 return False
1720 1722
1721 1723 def check_permissions(self, user, repo_name):
1722 1724 perms = user.permissions_with_scope({'repo_name': repo_name})
1723 1725
1724 1726 try:
1725 1727 user_perms = set([perms['repositories'][repo_name]])
1726 1728 except Exception:
1727 1729 log.exception('Error while accessing user permissions')
1728 1730 return False
1729 1731
1730 1732 if self.required_perms.intersection(user_perms):
1731 1733 return True
1732 1734 return False
1733 1735
1734 1736
1735 1737 # SPECIAL VERSION TO HANDLE API AUTH
1736 1738 class _BaseApiPerm(object):
1737 1739 def __init__(self, *perms):
1738 1740 self.required_perms = set(perms)
1739 1741
1740 1742 def __call__(self, check_location=None, user=None, repo_name=None,
1741 1743 group_name=None, user_group_name=None):
1742 1744 cls_name = self.__class__.__name__
1743 1745 check_scope = 'global:%s' % (self.required_perms,)
1744 1746 if repo_name:
1745 1747 check_scope += ', repo_name:%s' % (repo_name,)
1746 1748
1747 1749 if group_name:
1748 1750 check_scope += ', repo_group_name:%s' % (group_name,)
1749 1751
1750 1752 if user_group_name:
1751 1753 check_scope += ', user_group_name:%s' % (user_group_name,)
1752 1754
1753 1755 log.debug(
1754 1756 'checking cls:%s %s %s @ %s'
1755 1757 % (cls_name, self.required_perms, check_scope, check_location))
1756 1758 if not user:
1757 1759 log.debug('Empty User passed into arguments')
1758 1760 return False
1759 1761
1760 1762 # process user
1761 1763 if not isinstance(user, AuthUser):
1762 1764 user = AuthUser(user.user_id)
1763 1765 if not check_location:
1764 1766 check_location = 'unspecified'
1765 1767 if self.check_permissions(user.permissions, repo_name, group_name,
1766 1768 user_group_name):
1767 1769 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1768 1770 check_scope, user, check_location)
1769 1771 return True
1770 1772
1771 1773 else:
1772 1774 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1773 1775 check_scope, user, check_location)
1774 1776 return False
1775 1777
1776 1778 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1777 1779 user_group_name=None):
1778 1780 """
1779 1781 implement in child class should return True if permissions are ok,
1780 1782 False otherwise
1781 1783
1782 1784 :param perm_defs: dict with permission definitions
1783 1785 :param repo_name: repo name
1784 1786 """
1785 1787 raise NotImplementedError()
1786 1788
1787 1789
1788 1790 class HasPermissionAllApi(_BaseApiPerm):
1789 1791 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1790 1792 user_group_name=None):
1791 1793 if self.required_perms.issubset(perm_defs.get('global')):
1792 1794 return True
1793 1795 return False
1794 1796
1795 1797
1796 1798 class HasPermissionAnyApi(_BaseApiPerm):
1797 1799 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1798 1800 user_group_name=None):
1799 1801 if self.required_perms.intersection(perm_defs.get('global')):
1800 1802 return True
1801 1803 return False
1802 1804
1803 1805
1804 1806 class HasRepoPermissionAllApi(_BaseApiPerm):
1805 1807 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1806 1808 user_group_name=None):
1807 1809 try:
1808 1810 _user_perms = set([perm_defs['repositories'][repo_name]])
1809 1811 except KeyError:
1810 1812 log.warning(traceback.format_exc())
1811 1813 return False
1812 1814 if self.required_perms.issubset(_user_perms):
1813 1815 return True
1814 1816 return False
1815 1817
1816 1818
1817 1819 class HasRepoPermissionAnyApi(_BaseApiPerm):
1818 1820 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1819 1821 user_group_name=None):
1820 1822 try:
1821 1823 _user_perms = set([perm_defs['repositories'][repo_name]])
1822 1824 except KeyError:
1823 1825 log.warning(traceback.format_exc())
1824 1826 return False
1825 1827 if self.required_perms.intersection(_user_perms):
1826 1828 return True
1827 1829 return False
1828 1830
1829 1831
1830 1832 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1831 1833 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1832 1834 user_group_name=None):
1833 1835 try:
1834 1836 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1835 1837 except KeyError:
1836 1838 log.warning(traceback.format_exc())
1837 1839 return False
1838 1840 if self.required_perms.intersection(_user_perms):
1839 1841 return True
1840 1842 return False
1841 1843
1842 1844
1843 1845 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1844 1846 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1845 1847 user_group_name=None):
1846 1848 try:
1847 1849 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1848 1850 except KeyError:
1849 1851 log.warning(traceback.format_exc())
1850 1852 return False
1851 1853 if self.required_perms.issubset(_user_perms):
1852 1854 return True
1853 1855 return False
1854 1856
1855 1857
1856 1858 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
1857 1859 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
1858 1860 user_group_name=None):
1859 1861 try:
1860 1862 _user_perms = set([perm_defs['user_groups'][user_group_name]])
1861 1863 except KeyError:
1862 1864 log.warning(traceback.format_exc())
1863 1865 return False
1864 1866 if self.required_perms.intersection(_user_perms):
1865 1867 return True
1866 1868 return False
1867 1869
1868 1870
1869 1871 def check_ip_access(source_ip, allowed_ips=None):
1870 1872 """
1871 1873 Checks if source_ip is a subnet of any of allowed_ips.
1872 1874
1873 1875 :param source_ip:
1874 1876 :param allowed_ips: list of allowed ips together with mask
1875 1877 """
1876 1878 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1877 1879 source_ip_address = ipaddress.ip_address(source_ip)
1878 1880 if isinstance(allowed_ips, (tuple, list, set)):
1879 1881 for ip in allowed_ips:
1880 1882 try:
1881 1883 network_address = ipaddress.ip_network(ip, strict=False)
1882 1884 if source_ip_address in network_address:
1883 1885 log.debug('IP %s is network %s' %
1884 1886 (source_ip_address, network_address))
1885 1887 return True
1886 1888 # for any case we cannot determine the IP, don't crash just
1887 1889 # skip it and log as error, we want to say forbidden still when
1888 1890 # sending bad IP
1889 1891 except Exception:
1890 1892 log.error(traceback.format_exc())
1891 1893 continue
1892 1894 return False
1893 1895
1894 1896
1895 1897 def get_cython_compat_decorator(wrapper, func):
1896 1898 """
1897 1899 Creates a cython compatible decorator. The previously used
1898 1900 decorator.decorator() function seems to be incompatible with cython.
1899 1901
1900 1902 :param wrapper: __wrapper method of the decorator class
1901 1903 :param func: decorated function
1902 1904 """
1903 1905 @wraps(func)
1904 1906 def local_wrapper(*args, **kwds):
1905 1907 return wrapper(func, *args, **kwds)
1906 1908 local_wrapper.__wrapped__ = func
1907 1909 return local_wrapper
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,54 +1,75 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20 from rhodecode.model.auth_token import AuthTokenModel
21 21 from rhodecode.model.db import User
22 22 from rhodecode.tests import *
23 23
24 24
25 25 class TestFeedController(TestController):
26 26
27 27 def test_rss(self, backend):
28 28 self.log_user()
29 29 response = self.app.get(url(controller='feed', action='rss',
30 30 repo_name=backend.repo_name))
31 31
32 32 assert response.content_type == "application/rss+xml"
33 33 assert """<rss version="2.0">""" in response
34 34
35 def test_rss_with_auth_token(self, backend):
36 auth_token = User.get_first_super_admin().feed_token
35 def test_rss_with_auth_token(self, backend, user_admin):
36 auth_token = user_admin.feed_token
37 37 assert auth_token != ''
38 response = self.app.get(url(controller='feed', action='rss',
39 repo_name=backend.repo_name, auth_token=auth_token))
38 response = self.app.get(
39 url(controller='feed', action='rss',
40 repo_name=backend.repo_name, auth_token=auth_token,
41 status=200))
40 42
41 43 assert response.content_type == "application/rss+xml"
42 44 assert """<rss version="2.0">""" in response
43 45
46 def test_rss_with_auth_token_of_wrong_type(self, backend, user_util):
47 user = user_util.create_user()
48 auth_token = AuthTokenModel().create(
49 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
50 auth_token = auth_token.api_key
51
52 self.app.get(
53 url(controller='feed', action='rss',
54 repo_name=backend.repo_name, auth_token=auth_token),
55 status=302)
56
57 auth_token = AuthTokenModel().create(
58 user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
59 auth_token = auth_token.api_key
60 self.app.get(
61 url(controller='feed', action='rss',
62 repo_name=backend.repo_name, auth_token=auth_token),
63 status=200)
64
44 65 def test_atom(self, backend):
45 66 self.log_user()
46 67 response = self.app.get(url(controller='feed', action='atom',
47 68 repo_name=backend.repo_name))
48 69
49 70 assert response.content_type == """application/atom+xml"""
50 71 assert """<?xml version="1.0" encoding="utf-8"?>""" in response
51 72
52 73 tag1 = '<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-us">'
53 74 tag2 = '<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom">'
54 75 assert tag1 in response or tag2 in response
@@ -1,588 +1,591 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import urlparse
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.config.routing import ADMIN_PREFIX
27 27 from rhodecode.tests import (
28 28 TestController, assert_session_flash, clear_all_caches, url,
29 29 HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
30 30 from rhodecode.tests.fixture import Fixture
31 31 from rhodecode.tests.utils import AssertResponse, get_session_from_response
32 32 from rhodecode.lib.auth import check_password, generate_auth_token
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model.auth_token import AuthTokenModel
35 35 from rhodecode.model import validators
36 36 from rhodecode.model.db import User, Notification
37 37 from rhodecode.model.meta import Session
38 38
39 39 fixture = Fixture()
40 40
41 41 # Hardcode URLs because we don't have a request object to use
42 42 # pyramids URL generation methods.
43 43 index_url = '/'
44 44 login_url = ADMIN_PREFIX + '/login'
45 45 logut_url = ADMIN_PREFIX + '/logout'
46 46 register_url = ADMIN_PREFIX + '/register'
47 47 pwd_reset_url = ADMIN_PREFIX + '/password_reset'
48 48 pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
49 49
50 50
51 51 @pytest.mark.usefixtures('app')
52 52 class TestLoginController:
53 53 destroy_users = set()
54 54
55 55 @classmethod
56 56 def teardown_class(cls):
57 57 fixture.destroy_users(cls.destroy_users)
58 58
59 59 def teardown_method(self, method):
60 60 for n in Notification.query().all():
61 61 Session().delete(n)
62 62
63 63 Session().commit()
64 64 assert Notification.query().all() == []
65 65
66 66 def test_index(self):
67 67 response = self.app.get(login_url)
68 68 assert response.status == '200 OK'
69 69 # Test response...
70 70
71 71 def test_login_admin_ok(self):
72 72 response = self.app.post(login_url,
73 73 {'username': 'test_admin',
74 74 'password': 'test12'})
75 75 assert response.status == '302 Found'
76 76 session = get_session_from_response(response)
77 77 username = session['rhodecode_user'].get('username')
78 78 assert username == 'test_admin'
79 79 response = response.follow()
80 80 response.mustcontain('/%s' % HG_REPO)
81 81
82 82 def test_login_regular_ok(self):
83 83 response = self.app.post(login_url,
84 84 {'username': 'test_regular',
85 85 'password': 'test12'})
86 86
87 87 assert response.status == '302 Found'
88 88 session = get_session_from_response(response)
89 89 username = session['rhodecode_user'].get('username')
90 90 assert username == 'test_regular'
91 91 response = response.follow()
92 92 response.mustcontain('/%s' % HG_REPO)
93 93
94 94 def test_login_ok_came_from(self):
95 95 test_came_from = '/_admin/users?branch=stable'
96 96 _url = '{}?came_from={}'.format(login_url, test_came_from)
97 97 response = self.app.post(
98 98 _url, {'username': 'test_admin', 'password': 'test12'})
99 99 assert response.status == '302 Found'
100 100 assert 'branch=stable' in response.location
101 101 response = response.follow()
102 102
103 103 assert response.status == '200 OK'
104 104 response.mustcontain('Users administration')
105 105
106 106 def test_redirect_to_login_with_get_args(self):
107 107 with fixture.anon_access(False):
108 108 kwargs = {'branch': 'stable'}
109 109 response = self.app.get(
110 110 url('summary_home', repo_name=HG_REPO, **kwargs))
111 111 assert response.status == '302 Found'
112 112 response_query = urlparse.parse_qsl(response.location)
113 113 assert 'branch=stable' in response_query[0][1]
114 114
115 115 def test_login_form_with_get_args(self):
116 116 _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
117 117 response = self.app.get(_url)
118 118 assert 'branch%3Dstable' in response.form.action
119 119
120 120 @pytest.mark.parametrize("url_came_from", [
121 121 'data:text/html,<script>window.alert("xss")</script>',
122 122 'mailto:test@rhodecode.org',
123 123 'file:///etc/passwd',
124 124 'ftp://some.ftp.server',
125 125 'http://other.domain',
126 126 '/\r\nX-Forwarded-Host: http://example.org',
127 127 ])
128 128 def test_login_bad_came_froms(self, url_came_from):
129 129 _url = '{}?came_from={}'.format(login_url, url_came_from)
130 130 response = self.app.post(
131 131 _url,
132 132 {'username': 'test_admin', 'password': 'test12'})
133 133 assert response.status == '302 Found'
134 134 response = response.follow()
135 135 assert response.status == '200 OK'
136 136 assert response.request.path == '/'
137 137
138 138 def test_login_short_password(self):
139 139 response = self.app.post(login_url,
140 140 {'username': 'test_admin',
141 141 'password': 'as'})
142 142 assert response.status == '200 OK'
143 143
144 144 response.mustcontain('Enter 3 characters or more')
145 145
146 146 def test_login_wrong_non_ascii_password(self, user_regular):
147 147 response = self.app.post(
148 148 login_url,
149 149 {'username': user_regular.username,
150 150 'password': u'invalid-non-asci\xe4'.encode('utf8')})
151 151
152 152 response.mustcontain('invalid user name')
153 153 response.mustcontain('invalid password')
154 154
155 155 def test_login_with_non_ascii_password(self, user_util):
156 156 password = u'valid-non-ascii\xe4'
157 157 user = user_util.create_user(password=password)
158 158 response = self.app.post(
159 159 login_url,
160 160 {'username': user.username,
161 161 'password': password.encode('utf-8')})
162 162 assert response.status_code == 302
163 163
164 164 def test_login_wrong_username_password(self):
165 165 response = self.app.post(login_url,
166 166 {'username': 'error',
167 167 'password': 'test12'})
168 168
169 169 response.mustcontain('invalid user name')
170 170 response.mustcontain('invalid password')
171 171
172 172 def test_login_admin_ok_password_migration(self, real_crypto_backend):
173 173 from rhodecode.lib import auth
174 174
175 175 # create new user, with sha256 password
176 176 temp_user = 'test_admin_sha256'
177 177 user = fixture.create_user(temp_user)
178 178 user.password = auth._RhodeCodeCryptoSha256().hash_create(
179 179 b'test123')
180 180 Session().add(user)
181 181 Session().commit()
182 182 self.destroy_users.add(temp_user)
183 183 response = self.app.post(login_url,
184 184 {'username': temp_user,
185 185 'password': 'test123'})
186 186
187 187 assert response.status == '302 Found'
188 188 session = get_session_from_response(response)
189 189 username = session['rhodecode_user'].get('username')
190 190 assert username == temp_user
191 191 response = response.follow()
192 192 response.mustcontain('/%s' % HG_REPO)
193 193
194 194 # new password should be bcrypted, after log-in and transfer
195 195 user = User.get_by_username(temp_user)
196 196 assert user.password.startswith('$')
197 197
198 198 # REGISTRATIONS
199 199 def test_register(self):
200 200 response = self.app.get(register_url)
201 201 response.mustcontain('Create an Account')
202 202
203 203 def test_register_err_same_username(self):
204 204 uname = 'test_admin'
205 205 response = self.app.post(
206 206 register_url,
207 207 {
208 208 'username': uname,
209 209 'password': 'test12',
210 210 'password_confirmation': 'test12',
211 211 'email': 'goodmail@domain.com',
212 212 'firstname': 'test',
213 213 'lastname': 'test'
214 214 }
215 215 )
216 216
217 217 assertr = AssertResponse(response)
218 218 msg = validators.ValidUsername()._messages['username_exists']
219 219 msg = msg % {'username': uname}
220 220 assertr.element_contains('#username+.error-message', msg)
221 221
222 222 def test_register_err_same_email(self):
223 223 response = self.app.post(
224 224 register_url,
225 225 {
226 226 'username': 'test_admin_0',
227 227 'password': 'test12',
228 228 'password_confirmation': 'test12',
229 229 'email': 'test_admin@mail.com',
230 230 'firstname': 'test',
231 231 'lastname': 'test'
232 232 }
233 233 )
234 234
235 235 assertr = AssertResponse(response)
236 236 msg = validators.UniqSystemEmail()()._messages['email_taken']
237 237 assertr.element_contains('#email+.error-message', msg)
238 238
239 239 def test_register_err_same_email_case_sensitive(self):
240 240 response = self.app.post(
241 241 register_url,
242 242 {
243 243 'username': 'test_admin_1',
244 244 'password': 'test12',
245 245 'password_confirmation': 'test12',
246 246 'email': 'TesT_Admin@mail.COM',
247 247 'firstname': 'test',
248 248 'lastname': 'test'
249 249 }
250 250 )
251 251 assertr = AssertResponse(response)
252 252 msg = validators.UniqSystemEmail()()._messages['email_taken']
253 253 assertr.element_contains('#email+.error-message', msg)
254 254
255 255 def test_register_err_wrong_data(self):
256 256 response = self.app.post(
257 257 register_url,
258 258 {
259 259 'username': 'xs',
260 260 'password': 'test',
261 261 'password_confirmation': 'test',
262 262 'email': 'goodmailm',
263 263 'firstname': 'test',
264 264 'lastname': 'test'
265 265 }
266 266 )
267 267 assert response.status == '200 OK'
268 268 response.mustcontain('An email address must contain a single @')
269 269 response.mustcontain('Enter a value 6 characters long or more')
270 270
271 271 def test_register_err_username(self):
272 272 response = self.app.post(
273 273 register_url,
274 274 {
275 275 'username': 'error user',
276 276 'password': 'test12',
277 277 'password_confirmation': 'test12',
278 278 'email': 'goodmailm',
279 279 'firstname': 'test',
280 280 'lastname': 'test'
281 281 }
282 282 )
283 283
284 284 response.mustcontain('An email address must contain a single @')
285 285 response.mustcontain(
286 286 'Username may only contain '
287 287 'alphanumeric characters underscores, '
288 288 'periods or dashes and must begin with '
289 289 'alphanumeric character')
290 290
291 291 def test_register_err_case_sensitive(self):
292 292 usr = 'Test_Admin'
293 293 response = self.app.post(
294 294 register_url,
295 295 {
296 296 'username': usr,
297 297 'password': 'test12',
298 298 'password_confirmation': 'test12',
299 299 'email': 'goodmailm',
300 300 'firstname': 'test',
301 301 'lastname': 'test'
302 302 }
303 303 )
304 304
305 305 assertr = AssertResponse(response)
306 306 msg = validators.ValidUsername()._messages['username_exists']
307 307 msg = msg % {'username': usr}
308 308 assertr.element_contains('#username+.error-message', msg)
309 309
310 310 def test_register_special_chars(self):
311 311 response = self.app.post(
312 312 register_url,
313 313 {
314 314 'username': 'xxxaxn',
315 315 'password': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
316 316 'password_confirmation': 'Δ…Δ‡ΕΊΕΌΔ…Ε›Ε›Ε›Ε›',
317 317 'email': 'goodmailm@test.plx',
318 318 'firstname': 'test',
319 319 'lastname': 'test'
320 320 }
321 321 )
322 322
323 323 msg = validators.ValidPassword()._messages['invalid_password']
324 324 response.mustcontain(msg)
325 325
326 326 def test_register_password_mismatch(self):
327 327 response = self.app.post(
328 328 register_url,
329 329 {
330 330 'username': 'xs',
331 331 'password': '123qwe',
332 332 'password_confirmation': 'qwe123',
333 333 'email': 'goodmailm@test.plxa',
334 334 'firstname': 'test',
335 335 'lastname': 'test'
336 336 }
337 337 )
338 338 msg = validators.ValidPasswordsMatch()._messages['password_mismatch']
339 339 response.mustcontain(msg)
340 340
341 341 def test_register_ok(self):
342 342 username = 'test_regular4'
343 343 password = 'qweqwe'
344 344 email = 'marcin@test.com'
345 345 name = 'testname'
346 346 lastname = 'testlastname'
347 347
348 348 response = self.app.post(
349 349 register_url,
350 350 {
351 351 'username': username,
352 352 'password': password,
353 353 'password_confirmation': password,
354 354 'email': email,
355 355 'firstname': name,
356 356 'lastname': lastname,
357 357 'admin': True
358 358 }
359 359 ) # This should be overriden
360 360 assert response.status == '302 Found'
361 361 assert_session_flash(
362 362 response, 'You have successfully registered with RhodeCode')
363 363
364 364 ret = Session().query(User).filter(
365 365 User.username == 'test_regular4').one()
366 366 assert ret.username == username
367 367 assert check_password(password, ret.password)
368 368 assert ret.email == email
369 369 assert ret.name == name
370 370 assert ret.lastname == lastname
371 371 assert ret.api_key is not None
372 372 assert not ret.admin
373 373
374 374 def test_forgot_password_wrong_mail(self):
375 375 bad_email = 'marcin@wrongmail.org'
376 376 response = self.app.post(
377 377 pwd_reset_url,
378 378 {'email': bad_email, }
379 379 )
380 380
381 381 msg = validators.ValidSystemEmail()._messages['non_existing_email']
382 382 msg = h.html_escape(msg % {'email': bad_email})
383 383 response.mustcontain()
384 384
385 385 def test_forgot_password(self):
386 386 response = self.app.get(pwd_reset_url)
387 387 assert response.status == '200 OK'
388 388
389 389 username = 'test_password_reset_1'
390 390 password = 'qweqwe'
391 391 email = 'marcin@python-works.com'
392 392 name = 'passwd'
393 393 lastname = 'reset'
394 394
395 395 new = User()
396 396 new.username = username
397 397 new.password = password
398 398 new.email = email
399 399 new.name = name
400 400 new.lastname = lastname
401 401 new.api_key = generate_auth_token(username)
402 402 Session().add(new)
403 403 Session().commit()
404 404
405 405 response = self.app.post(pwd_reset_url,
406 406 {'email': email, })
407 407
408 408 assert_session_flash(
409 409 response, 'Your password reset link was sent')
410 410
411 411 response = response.follow()
412 412
413 413 # BAD KEY
414 414
415 415 key = "bad"
416 416 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
417 417 response = self.app.get(confirm_url)
418 418 assert response.status == '302 Found'
419 419 assert response.location.endswith(pwd_reset_url)
420 420
421 421 # GOOD KEY
422 422
423 423 key = User.get_by_username(username).api_key
424 424 confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key)
425 425 response = self.app.get(confirm_url)
426 426 assert response.status == '302 Found'
427 427 assert response.location.endswith(login_url)
428 428
429 429 assert_session_flash(
430 430 response,
431 431 'Your password reset was successful, '
432 432 'a new password has been sent to your email')
433 433
434 434 response = response.follow()
435 435
436 436 def _get_api_whitelist(self, values=None):
437 437 config = {'api_access_controllers_whitelist': values or []}
438 438 return config
439 439
440 440 @pytest.mark.parametrize("test_name, auth_token", [
441 441 ('none', None),
442 442 ('empty_string', ''),
443 443 ('fake_number', '123456'),
444 444 ('proper_auth_token', None)
445 445 ])
446 def test_access_not_whitelisted_page_via_auth_token(self, test_name,
447 auth_token):
446 def test_access_not_whitelisted_page_via_auth_token(
447 self, test_name, auth_token, user_admin):
448
448 449 whitelist = self._get_api_whitelist([])
449 450 with mock.patch.dict('rhodecode.CONFIG', whitelist):
450 451 assert [] == whitelist['api_access_controllers_whitelist']
451 452 if test_name == 'proper_auth_token':
452 453 # use builtin if api_key is None
453 auth_token = User.get_first_super_admin().api_key
454 auth_token = user_admin.api_key
454 455
455 456 with fixture.anon_access(False):
456 457 self.app.get(url(controller='changeset',
457 458 action='changeset_raw',
458 459 repo_name=HG_REPO, revision='tip',
459 460 api_key=auth_token),
460 461 status=302)
461 462
462 463 @pytest.mark.parametrize("test_name, auth_token, code", [
463 464 ('none', None, 302),
464 465 ('empty_string', '', 302),
465 466 ('fake_number', '123456', 302),
466 467 ('proper_auth_token', None, 200)
467 468 ])
468 def test_access_whitelisted_page_via_auth_token(self, test_name,
469 auth_token, code):
470 whitelist = self._get_api_whitelist(
471 ['ChangesetController:changeset_raw'])
469 def test_access_whitelisted_page_via_auth_token(
470 self, test_name, auth_token, code, user_admin):
471
472 whitelist_entry = ['ChangesetController:changeset_raw']
473 whitelist = self._get_api_whitelist(whitelist_entry)
474
472 475 with mock.patch.dict('rhodecode.CONFIG', whitelist):
473 assert ['ChangesetController:changeset_raw'] == \
474 whitelist['api_access_controllers_whitelist']
476 assert whitelist_entry == whitelist['api_access_controllers_whitelist']
477
475 478 if test_name == 'proper_auth_token':
476 auth_token = User.get_first_super_admin().api_key
479 auth_token = user_admin.api_key
477 480
478 481 with fixture.anon_access(False):
479 482 self.app.get(url(controller='changeset',
480 483 action='changeset_raw',
481 484 repo_name=HG_REPO, revision='tip',
482 485 api_key=auth_token),
483 486 status=code)
484 487
485 488 def test_access_page_via_extra_auth_token(self):
486 489 whitelist = self._get_api_whitelist(
487 490 ['ChangesetController:changeset_raw'])
488 491 with mock.patch.dict('rhodecode.CONFIG', whitelist):
489 492 assert ['ChangesetController:changeset_raw'] == \
490 493 whitelist['api_access_controllers_whitelist']
491 494
492 495 new_auth_token = AuthTokenModel().create(
493 496 TEST_USER_ADMIN_LOGIN, 'test')
494 497 Session().commit()
495 498 with fixture.anon_access(False):
496 499 self.app.get(url(controller='changeset',
497 500 action='changeset_raw',
498 501 repo_name=HG_REPO, revision='tip',
499 502 api_key=new_auth_token.api_key),
500 503 status=200)
501 504
502 505 def test_access_page_via_expired_auth_token(self):
503 506 whitelist = self._get_api_whitelist(
504 507 ['ChangesetController:changeset_raw'])
505 508 with mock.patch.dict('rhodecode.CONFIG', whitelist):
506 509 assert ['ChangesetController:changeset_raw'] == \
507 510 whitelist['api_access_controllers_whitelist']
508 511
509 512 new_auth_token = AuthTokenModel().create(
510 513 TEST_USER_ADMIN_LOGIN, 'test')
511 514 Session().commit()
512 515 # patch the api key and make it expired
513 516 new_auth_token.expires = 0
514 517 Session().add(new_auth_token)
515 518 Session().commit()
516 519 with fixture.anon_access(False):
517 520 self.app.get(url(controller='changeset',
518 521 action='changeset_raw',
519 522 repo_name=HG_REPO, revision='tip',
520 523 api_key=new_auth_token.api_key),
521 524 status=302)
522 525
523 526
524 527 class TestPasswordReset(TestController):
525 528
526 529 @pytest.mark.parametrize(
527 530 'pwd_reset_setting, show_link, show_reset', [
528 531 ('hg.password_reset.enabled', True, True),
529 532 ('hg.password_reset.hidden', False, True),
530 533 ('hg.password_reset.disabled', False, False),
531 534 ])
532 535 def test_password_reset_settings(
533 536 self, pwd_reset_setting, show_link, show_reset):
534 537 clear_all_caches()
535 538 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
536 539 params = {
537 540 'csrf_token': self.csrf_token,
538 541 'anonymous': 'True',
539 542 'default_register': 'hg.register.auto_activate',
540 543 'default_register_message': '',
541 544 'default_password_reset': pwd_reset_setting,
542 545 'default_extern_activate': 'hg.extern_activate.auto',
543 546 }
544 547 resp = self.app.post(url('admin_permissions_application'), params=params)
545 548 self.logout_user()
546 549
547 550 login_page = self.app.get(login_url)
548 551 asr_login = AssertResponse(login_page)
549 552 index_page = self.app.get(index_url)
550 553 asr_index = AssertResponse(index_page)
551 554
552 555 if show_link:
553 556 asr_login.one_element_exists('a.pwd_reset')
554 557 asr_index.one_element_exists('a.pwd_reset')
555 558 else:
556 559 asr_login.no_element_exists('a.pwd_reset')
557 560 asr_index.no_element_exists('a.pwd_reset')
558 561
559 562 pwdreset_page = self.app.get(pwd_reset_url)
560 563
561 564 asr_reset = AssertResponse(pwdreset_page)
562 565 if show_reset:
563 566 assert 'Send password reset email' in pwdreset_page
564 567 asr_reset.one_element_exists('#email')
565 568 asr_reset.one_element_exists('#send')
566 569 else:
567 570 assert 'Password reset is disabled.' in pwdreset_page
568 571 asr_reset.no_element_exists('#email')
569 572 asr_reset.no_element_exists('#send')
570 573
571 574 def test_password_form_disabled(self):
572 575 self.log_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
573 576 params = {
574 577 'csrf_token': self.csrf_token,
575 578 'anonymous': 'True',
576 579 'default_register': 'hg.register.auto_activate',
577 580 'default_register_message': '',
578 581 'default_password_reset': 'hg.password_reset.disabled',
579 582 'default_extern_activate': 'hg.extern_activate.auto',
580 583 }
581 584 self.app.post(url('admin_permissions_application'), params=params)
582 585 self.logout_user()
583 586
584 587 pwdreset_page = self.app.post(
585 588 pwd_reset_url,
586 589 {'email': 'lisa@rhodecode.com',}
587 590 )
588 591 assert 'Password reset is disabled.' in pwdreset_page
@@ -1,582 +1,608 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 from hashlib import sha1
23 23
24 24 import pytest
25 25 from mock import patch
26 26
27 27 from rhodecode.lib import auth
28 28 from rhodecode.lib.utils2 import md5
29 from rhodecode.model.auth_token import AuthTokenModel
29 30 from rhodecode.model.db import User
30 31 from rhodecode.model.repo import RepoModel
31 32 from rhodecode.model.user import UserModel
32 33 from rhodecode.model.user_group import UserGroupModel
33 34
34 35
35 36 def test_perm_origin_dict():
36 37 pod = auth.PermOriginDict()
37 38 pod['thing'] = 'read', 'default'
38 39 assert pod['thing'] == 'read'
39 40
40 41 assert pod.perm_origin_stack == {
41 42 'thing': [('read', 'default')]}
42 43
43 44 pod['thing'] = 'write', 'admin'
44 45 assert pod['thing'] == 'write'
45 46
46 47 assert pod.perm_origin_stack == {
47 48 'thing': [('read', 'default'), ('write', 'admin')]}
48 49
49 50 pod['other'] = 'write', 'default'
50 51
51 52 assert pod.perm_origin_stack == {
52 53 'other': [('write', 'default')],
53 54 'thing': [('read', 'default'), ('write', 'admin')]}
54 55
55 56 pod['other'] = 'none', 'override'
56 57
57 58 assert pod.perm_origin_stack == {
58 59 'other': [('write', 'default'), ('none', 'override')],
59 60 'thing': [('read', 'default'), ('write', 'admin')]}
60 61
61 62 with pytest.raises(ValueError):
62 63 pod['thing'] = 'read'
63 64
64 65
65 66 def test_cached_perms_data(user_regular, backend_random):
66 67 permissions = get_permissions(user_regular)
67 68 repo_name = backend_random.repo.repo_name
68 69 expected_global_permissions = {
69 70 'repository.read', 'group.read', 'usergroup.read'}
70 71 assert expected_global_permissions.issubset(permissions['global'])
71 72 assert permissions['repositories'][repo_name] == 'repository.read'
72 73
73 74
74 75 def test_cached_perms_data_with_admin_user(user_regular, backend_random):
75 76 permissions = get_permissions(user_regular, user_is_admin=True)
76 77 repo_name = backend_random.repo.repo_name
77 78 assert 'hg.admin' in permissions['global']
78 79 assert permissions['repositories'][repo_name] == 'repository.admin'
79 80
80 81
81 82 def test_cached_perms_data_user_group_global_permissions(user_util):
82 83 user, user_group = user_util.create_user_with_group()
83 84 user_group.inherit_default_permissions = False
84 85
85 86 granted_permission = 'repository.write'
86 87 UserGroupModel().grant_perm(user_group, granted_permission)
87 88
88 89 permissions = get_permissions(user)
89 90 assert granted_permission in permissions['global']
90 91
91 92
92 93 @pytest.mark.xfail(reason="Not implemented, see TODO note")
93 94 def test_cached_perms_data_user_group_global_permissions_(user_util):
94 95 user, user_group = user_util.create_user_with_group()
95 96
96 97 granted_permission = 'repository.write'
97 98 UserGroupModel().grant_perm(user_group, granted_permission)
98 99
99 100 permissions = get_permissions(user)
100 101 assert granted_permission in permissions['global']
101 102
102 103
103 104 def test_cached_perms_data_user_global_permissions(user_util):
104 105 user = user_util.create_user()
105 106 UserModel().grant_perm(user, 'repository.none')
106 107
107 108 permissions = get_permissions(user, user_inherit_default_permissions=True)
108 109 assert 'repository.read' in permissions['global']
109 110
110 111
111 112 def test_cached_perms_data_repository_permissions_on_private_repository(
112 113 backend_random, user_util):
113 114 user, user_group = user_util.create_user_with_group()
114 115
115 116 repo = backend_random.create_repo()
116 117 repo.private = True
117 118
118 119 granted_permission = 'repository.write'
119 120 RepoModel().grant_user_group_permission(
120 121 repo, user_group.users_group_name, granted_permission)
121 122
122 123 permissions = get_permissions(user)
123 124 assert permissions['repositories'][repo.repo_name] == granted_permission
124 125
125 126
126 127 def test_cached_perms_data_repository_permissions_for_owner(
127 128 backend_random, user_util):
128 129 user = user_util.create_user()
129 130
130 131 repo = backend_random.create_repo()
131 132 repo.user_id = user.user_id
132 133
133 134 permissions = get_permissions(user)
134 135 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
135 136
136 137 # TODO: johbo: Make cleanup in UserUtility smarter, then remove this hack
137 138 repo.user_id = User.get_default_user().user_id
138 139
139 140
140 141 def test_cached_perms_data_repository_permissions_not_inheriting_defaults(
141 142 backend_random, user_util):
142 143 user = user_util.create_user()
143 144 repo = backend_random.create_repo()
144 145
145 146 # Don't inherit default object permissions
146 147 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
147 148
148 149 permissions = get_permissions(user)
149 150 assert permissions['repositories'][repo.repo_name] == 'repository.none'
150 151
151 152
152 153 def test_cached_perms_data_default_permissions_on_repository_group(user_util):
153 154 # Have a repository group with default permissions set
154 155 repo_group = user_util.create_repo_group()
155 156 default_user = User.get_default_user()
156 157 user_util.grant_user_permission_to_repo_group(
157 158 repo_group, default_user, 'repository.write')
158 159 user = user_util.create_user()
159 160
160 161 permissions = get_permissions(user)
161 162 assert permissions['repositories_groups'][repo_group.group_name] == \
162 163 'repository.write'
163 164
164 165
165 166 def test_cached_perms_data_default_permissions_on_repository_group_owner(
166 167 user_util):
167 168 # Have a repository group
168 169 repo_group = user_util.create_repo_group()
169 170 default_user = User.get_default_user()
170 171
171 172 # Add a permission for the default user to hit the code path
172 173 user_util.grant_user_permission_to_repo_group(
173 174 repo_group, default_user, 'repository.write')
174 175
175 176 # Have an owner of the group
176 177 user = user_util.create_user()
177 178 repo_group.user_id = user.user_id
178 179
179 180 permissions = get_permissions(user)
180 181 assert permissions['repositories_groups'][repo_group.group_name] == \
181 182 'group.admin'
182 183
183 184
184 185 def test_cached_perms_data_default_permissions_on_repository_group_no_inherit(
185 186 user_util):
186 187 # Have a repository group
187 188 repo_group = user_util.create_repo_group()
188 189 default_user = User.get_default_user()
189 190
190 191 # Add a permission for the default user to hit the code path
191 192 user_util.grant_user_permission_to_repo_group(
192 193 repo_group, default_user, 'repository.write')
193 194
194 195 # Don't inherit default object permissions
195 196 user = user_util.create_user()
196 197 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
197 198
198 199 permissions = get_permissions(user)
199 200 assert permissions['repositories_groups'][repo_group.group_name] == \
200 201 'group.none'
201 202
202 203
203 204 def test_cached_perms_data_repository_permissions_from_user_group(
204 205 user_util, backend_random):
205 206 user, user_group = user_util.create_user_with_group()
206 207
207 208 # Needs a second user group to make sure that we select the right
208 209 # permissions.
209 210 user_group2 = user_util.create_user_group()
210 211 UserGroupModel().add_user_to_group(user_group2, user)
211 212
212 213 repo = backend_random.create_repo()
213 214
214 215 RepoModel().grant_user_group_permission(
215 216 repo, user_group.users_group_name, 'repository.read')
216 217 RepoModel().grant_user_group_permission(
217 218 repo, user_group2.users_group_name, 'repository.write')
218 219
219 220 permissions = get_permissions(user)
220 221 assert permissions['repositories'][repo.repo_name] == 'repository.write'
221 222
222 223
223 224 def test_cached_perms_data_repository_permissions_from_user_group_owner(
224 225 user_util, backend_random):
225 226 user, user_group = user_util.create_user_with_group()
226 227
227 228 repo = backend_random.create_repo()
228 229 repo.user_id = user.user_id
229 230
230 231 RepoModel().grant_user_group_permission(
231 232 repo, user_group.users_group_name, 'repository.write')
232 233
233 234 permissions = get_permissions(user)
234 235 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
235 236
236 237
237 238 def test_cached_perms_data_user_repository_permissions(
238 239 user_util, backend_random):
239 240 user = user_util.create_user()
240 241 repo = backend_random.create_repo()
241 242 granted_permission = 'repository.write'
242 243 RepoModel().grant_user_permission(repo, user, granted_permission)
243 244
244 245 permissions = get_permissions(user)
245 246 assert permissions['repositories'][repo.repo_name] == granted_permission
246 247
247 248
248 249 def test_cached_perms_data_user_repository_permissions_explicit(
249 250 user_util, backend_random):
250 251 user = user_util.create_user()
251 252 repo = backend_random.create_repo()
252 253 granted_permission = 'repository.none'
253 254 RepoModel().grant_user_permission(repo, user, granted_permission)
254 255
255 256 permissions = get_permissions(user, explicit=True)
256 257 assert permissions['repositories'][repo.repo_name] == granted_permission
257 258
258 259
259 260 def test_cached_perms_data_user_repository_permissions_owner(
260 261 user_util, backend_random):
261 262 user = user_util.create_user()
262 263 repo = backend_random.create_repo()
263 264 repo.user_id = user.user_id
264 265 RepoModel().grant_user_permission(repo, user, 'repository.write')
265 266
266 267 permissions = get_permissions(user)
267 268 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
268 269
269 270
270 271 def test_cached_perms_data_repository_groups_permissions_inherited(
271 272 user_util, backend_random):
272 273 user, user_group = user_util.create_user_with_group()
273 274
274 275 # Needs a second group to hit the last condition
275 276 user_group2 = user_util.create_user_group()
276 277 UserGroupModel().add_user_to_group(user_group2, user)
277 278
278 279 repo_group = user_util.create_repo_group()
279 280
280 281 user_util.grant_user_group_permission_to_repo_group(
281 282 repo_group, user_group, 'group.read')
282 283 user_util.grant_user_group_permission_to_repo_group(
283 284 repo_group, user_group2, 'group.write')
284 285
285 286 permissions = get_permissions(user)
286 287 assert permissions['repositories_groups'][repo_group.group_name] == \
287 288 'group.write'
288 289
289 290
290 291 def test_cached_perms_data_repository_groups_permissions_inherited_owner(
291 292 user_util, backend_random):
292 293 user, user_group = user_util.create_user_with_group()
293 294 repo_group = user_util.create_repo_group()
294 295 repo_group.user_id = user.user_id
295 296
296 297 granted_permission = 'group.write'
297 298 user_util.grant_user_group_permission_to_repo_group(
298 299 repo_group, user_group, granted_permission)
299 300
300 301 permissions = get_permissions(user)
301 302 assert permissions['repositories_groups'][repo_group.group_name] == \
302 303 'group.admin'
303 304
304 305
305 306 def test_cached_perms_data_repository_groups_permissions(
306 307 user_util, backend_random):
307 308 user = user_util.create_user()
308 309
309 310 repo_group = user_util.create_repo_group()
310 311
311 312 granted_permission = 'group.write'
312 313 user_util.grant_user_permission_to_repo_group(
313 314 repo_group, user, granted_permission)
314 315
315 316 permissions = get_permissions(user)
316 317 assert permissions['repositories_groups'][repo_group.group_name] == \
317 318 'group.write'
318 319
319 320
320 321 def test_cached_perms_data_repository_groups_permissions_explicit(
321 322 user_util, backend_random):
322 323 user = user_util.create_user()
323 324
324 325 repo_group = user_util.create_repo_group()
325 326
326 327 granted_permission = 'group.none'
327 328 user_util.grant_user_permission_to_repo_group(
328 329 repo_group, user, granted_permission)
329 330
330 331 permissions = get_permissions(user, explicit=True)
331 332 assert permissions['repositories_groups'][repo_group.group_name] == \
332 333 'group.none'
333 334
334 335
335 336 def test_cached_perms_data_repository_groups_permissions_owner(
336 337 user_util, backend_random):
337 338 user = user_util.create_user()
338 339
339 340 repo_group = user_util.create_repo_group()
340 341 repo_group.user_id = user.user_id
341 342
342 343 granted_permission = 'group.write'
343 344 user_util.grant_user_permission_to_repo_group(
344 345 repo_group, user, granted_permission)
345 346
346 347 permissions = get_permissions(user)
347 348 assert permissions['repositories_groups'][repo_group.group_name] == \
348 349 'group.admin'
349 350
350 351
351 352 def test_cached_perms_data_user_group_permissions_inherited(
352 353 user_util, backend_random):
353 354 user, user_group = user_util.create_user_with_group()
354 355 user_group2 = user_util.create_user_group()
355 356 UserGroupModel().add_user_to_group(user_group2, user)
356 357
357 358 target_user_group = user_util.create_user_group()
358 359
359 360 user_util.grant_user_group_permission_to_user_group(
360 361 target_user_group, user_group, 'usergroup.read')
361 362 user_util.grant_user_group_permission_to_user_group(
362 363 target_user_group, user_group2, 'usergroup.write')
363 364
364 365 permissions = get_permissions(user)
365 366 assert permissions['user_groups'][target_user_group.users_group_name] == \
366 367 'usergroup.write'
367 368
368 369
369 370 def test_cached_perms_data_user_group_permissions(
370 371 user_util, backend_random):
371 372 user = user_util.create_user()
372 373 user_group = user_util.create_user_group()
373 374 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.write')
374 375
375 376 permissions = get_permissions(user)
376 377 assert permissions['user_groups'][user_group.users_group_name] == \
377 378 'usergroup.write'
378 379
379 380
380 381 def test_cached_perms_data_user_group_permissions_explicit(
381 382 user_util, backend_random):
382 383 user = user_util.create_user()
383 384 user_group = user_util.create_user_group()
384 385 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.none')
385 386
386 387 permissions = get_permissions(user, explicit=True)
387 388 assert permissions['user_groups'][user_group.users_group_name] == \
388 389 'usergroup.none'
389 390
390 391
391 392 def test_cached_perms_data_user_group_permissions_not_inheriting_defaults(
392 393 user_util, backend_random):
393 394 user = user_util.create_user()
394 395 user_group = user_util.create_user_group()
395 396
396 397 # Don't inherit default object permissions
397 398 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
398 399
399 400 permissions = get_permissions(user)
400 401 assert permissions['user_groups'][user_group.users_group_name] == \
401 402 'usergroup.none'
402 403
403 404
404 405 def test_permission_calculator_admin_permissions(
405 406 user_util, backend_random):
406 407 user = user_util.create_user()
407 408 user_group = user_util.create_user_group()
408 409 repo = backend_random.repo
409 410 repo_group = user_util.create_repo_group()
410 411
411 412 calculator = auth.PermissionCalculator(
412 413 user.user_id, {}, False, False, True, 'higherwin')
413 414 permissions = calculator._admin_permissions()
414 415
415 416 assert permissions['repositories_groups'][repo_group.group_name] == \
416 417 'group.admin'
417 418 assert permissions['user_groups'][user_group.users_group_name] == \
418 419 'usergroup.admin'
419 420 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
420 421 assert 'hg.admin' in permissions['global']
421 422
422 423
423 424 def test_permission_calculator_repository_permissions_robustness_from_group(
424 425 user_util, backend_random):
425 426 user, user_group = user_util.create_user_with_group()
426 427
427 428 RepoModel().grant_user_group_permission(
428 429 backend_random.repo, user_group.users_group_name, 'repository.write')
429 430
430 431 calculator = auth.PermissionCalculator(
431 432 user.user_id, {}, False, False, False, 'higherwin')
432 433 calculator._calculate_repository_permissions()
433 434
434 435
435 436 def test_permission_calculator_repository_permissions_robustness_from_user(
436 437 user_util, backend_random):
437 438 user = user_util.create_user()
438 439
439 440 RepoModel().grant_user_permission(
440 441 backend_random.repo, user, 'repository.write')
441 442
442 443 calculator = auth.PermissionCalculator(
443 444 user.user_id, {}, False, False, False, 'higherwin')
444 445 calculator._calculate_repository_permissions()
445 446
446 447
447 448 def test_permission_calculator_repo_group_permissions_robustness_from_group(
448 449 user_util, backend_random):
449 450 user, user_group = user_util.create_user_with_group()
450 451 repo_group = user_util.create_repo_group()
451 452
452 453 user_util.grant_user_group_permission_to_repo_group(
453 454 repo_group, user_group, 'group.write')
454 455
455 456 calculator = auth.PermissionCalculator(
456 457 user.user_id, {}, False, False, False, 'higherwin')
457 458 calculator._calculate_repository_group_permissions()
458 459
459 460
460 461 def test_permission_calculator_repo_group_permissions_robustness_from_user(
461 462 user_util, backend_random):
462 463 user = user_util.create_user()
463 464 repo_group = user_util.create_repo_group()
464 465
465 466 user_util.grant_user_permission_to_repo_group(
466 467 repo_group, user, 'group.write')
467 468
468 469 calculator = auth.PermissionCalculator(
469 470 user.user_id, {}, False, False, False, 'higherwin')
470 471 calculator._calculate_repository_group_permissions()
471 472
472 473
473 474 def test_permission_calculator_user_group_permissions_robustness_from_group(
474 475 user_util, backend_random):
475 476 user, user_group = user_util.create_user_with_group()
476 477 target_user_group = user_util.create_user_group()
477 478
478 479 user_util.grant_user_group_permission_to_user_group(
479 480 target_user_group, user_group, 'usergroup.write')
480 481
481 482 calculator = auth.PermissionCalculator(
482 483 user.user_id, {}, False, False, False, 'higherwin')
483 484 calculator._calculate_user_group_permissions()
484 485
485 486
486 487 def test_permission_calculator_user_group_permissions_robustness_from_user(
487 488 user_util, backend_random):
488 489 user = user_util.create_user()
489 490 target_user_group = user_util.create_user_group()
490 491
491 492 user_util.grant_user_permission_to_user_group(
492 493 target_user_group, user, 'usergroup.write')
493 494
494 495 calculator = auth.PermissionCalculator(
495 496 user.user_id, {}, False, False, False, 'higherwin')
496 497 calculator._calculate_user_group_permissions()
497 498
498 499
499 500 @pytest.mark.parametrize("algo, new_permission, old_permission, expected", [
500 501 ('higherwin', 'repository.none', 'repository.none', 'repository.none'),
501 502 ('higherwin', 'repository.read', 'repository.none', 'repository.read'),
502 503 ('lowerwin', 'repository.write', 'repository.write', 'repository.write'),
503 504 ('lowerwin', 'repository.read', 'repository.write', 'repository.read'),
504 505 ])
505 506 def test_permission_calculator_choose_permission(
506 507 user_regular, algo, new_permission, old_permission, expected):
507 508 calculator = auth.PermissionCalculator(
508 509 user_regular.user_id, {}, False, False, False, algo)
509 510 result = calculator._choose_permission(new_permission, old_permission)
510 511 assert result == expected
511 512
512 513
513 514 def test_permission_calculator_choose_permission_raises_on_wrong_algo(
514 515 user_regular):
515 516 calculator = auth.PermissionCalculator(
516 517 user_regular.user_id, {}, False, False, False, 'invalid')
517 518 result = calculator._choose_permission(
518 519 'repository.read', 'repository.read')
519 520 # TODO: johbo: This documents the existing behavior. Think of an
520 521 # improvement.
521 522 assert result is None
522 523
523 524
524 525 def test_auth_user_get_cookie_store_for_normal_user(user_util):
525 526 user = user_util.create_user()
526 527 auth_user = auth.AuthUser(user_id=user.user_id)
527 528 expected_data = {
528 529 'username': user.username,
529 530 'user_id': user.user_id,
530 531 'password': md5(user.password),
531 532 'is_authenticated': False
532 533 }
533 534 assert auth_user.get_cookie_store() == expected_data
534 535
535 536
536 537 def test_auth_user_get_cookie_store_for_default_user():
537 538 default_user = User.get_default_user()
538 539 auth_user = auth.AuthUser()
539 540 expected_data = {
540 541 'username': User.DEFAULT_USER,
541 542 'user_id': default_user.user_id,
542 543 'password': md5(default_user.password),
543 544 'is_authenticated': True
544 545 }
545 546 assert auth_user.get_cookie_store() == expected_data
546 547
547 548
548 549 def get_permissions(user, **kwargs):
549 550 """
550 551 Utility filling in useful defaults into the call to `_cached_perms_data`.
551 552
552 553 Fill in `**kwargs` if specific values are needed for a test.
553 554 """
554 555 call_args = {
555 556 'user_id': user.user_id,
556 557 'scope': {},
557 558 'user_is_admin': False,
558 559 'user_inherit_default_permissions': False,
559 560 'explicit': False,
560 561 'algo': 'higherwin',
561 562 }
562 563 call_args.update(kwargs)
563 564 permissions = auth._cached_perms_data(**call_args)
564 565 return permissions
565 566
566 567
567 568 class TestGenerateAuthToken(object):
568 569 def test_salt_is_used_when_specified(self):
569 570 salt = 'abcde'
570 571 user_name = 'test_user'
571 572 result = auth.generate_auth_token(user_name, salt)
572 573 expected_result = sha1(user_name + salt).hexdigest()
573 574 assert result == expected_result
574 575
575 576 def test_salt_is_geneated_when_not_specified(self):
576 577 user_name = 'test_user'
577 578 random_salt = os.urandom(16)
578 579 with patch.object(auth, 'os') as os_mock:
579 580 os_mock.urandom.return_value = random_salt
580 581 result = auth.generate_auth_token(user_name)
581 582 expected_result = sha1(user_name + random_salt).hexdigest()
582 583 assert result == expected_result
584
585
586 @pytest.mark.parametrize("test_token, test_roles, auth_result, expected_tokens", [
587 ('', None, False,
588 []),
589 ('wrongtoken', None, False,
590 []),
591 ('abracadabra_vcs', [AuthTokenModel.cls.ROLE_API], False,
592 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
593 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
594 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
595 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
596 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1),
597 ('abracadabra_http', AuthTokenModel.cls.ROLE_HTTP, -1)]),
598 ])
599 def test_auth_by_token(test_token, test_roles, auth_result, expected_tokens,
600 user_util):
601 user = user_util.create_user()
602 user_id = user.user_id
603 for token, role, expires in expected_tokens:
604 new_token = AuthTokenModel().create(user_id, 'test-token', expires, role)
605 new_token.api_key = token # inject known name for testing...
606
607 assert auth_result == user.authenticate_by_token(
608 test_token, roles=test_roles, include_builtin_token=True)
General Comments 0
You need to be logged in to leave comments. Login now