##// END OF EJS Templates
merged freebsd support issue from default
marcink -
r1195:74251f80 beta
parent child Browse files
Show More
@@ -1,591 +1,588 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.auth
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 9 :copyright: (c) 2010 by marcink.
10 10 :license: LICENSE_NAME, see LICENSE_FILE for more details.
11 11 """
12 12 # This program is free software; you can redistribute it and/or
13 13 # modify it under the terms of the GNU General Public License
14 14 # as published by the Free Software Foundation; version 2
15 15 # of the License or (at your opinion) any later version of the license.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program; if not, write to the Free Software
24 24 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
25 25 # MA 02110-1301, USA.
26 26
27 27 import random
28 28 import logging
29 29 import traceback
30 30 import hashlib
31 31
32 32 from tempfile import _RandomNameSequence
33 33 from decorator import decorator
34 34
35 35 from pylons import config, session, url, request
36 36 from pylons.controllers.util import abort, redirect
37 37 from pylons.i18n.translation import _
38 38
39 from rhodecode import __platform__
39 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
40 40
41 if __platform__ == 'Windows':
41 if __platform__ in PLATFORM_WIN:
42 42 from hashlib import sha256
43 if __platform__ in ('Linux', 'Darwin'):
43 if __platform__ in PLATFORM_OTHERS:
44 44 import bcrypt
45 45
46 46 from rhodecode.lib import str2bool
47 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
48 48 from rhodecode.lib.utils import get_repo_slug
49 49 from rhodecode.lib.auth_ldap import AuthLdap
50 50
51 51 from rhodecode.model import meta
52 52 from rhodecode.model.user import UserModel
53 53 from rhodecode.model.db import Permission
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58 class PasswordGenerator(object):
59 59 """This is a simple class for generating password from
60 60 different sets of characters
61 61 usage:
62 62 passwd_gen = PasswordGenerator()
63 63 #print 8-letter password containing only big and small letters of alphabet
64 64 print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
65 65 """
66 66 ALPHABETS_NUM = r'''1234567890'''#[0]
67 67 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''#[1]
68 68 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''#[2]
69 69 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?''' #[3]
70 70 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM + ALPHABETS_SPECIAL#[4]
71 71 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM#[5]
72 72 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
73 73 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM#[6]
74 74 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM#[7]
75 75
76 76 def __init__(self, passwd=''):
77 77 self.passwd = passwd
78 78
79 79 def gen_password(self, len, type):
80 80 self.passwd = ''.join([random.choice(type) for _ in xrange(len)])
81 81 return self.passwd
82 82
83 83 class RhodeCodeCrypto(object):
84 84
85 85 @classmethod
86 86 def hash_string(cls, str_):
87 87 """
88 88 Cryptographic function used for password hashing based on pybcrypt
89 89 or pycrypto in windows
90 90
91 91 :param password: password to hash
92 92 """
93 if __platform__ == 'Windows':
93 if __platform__ in PLATFORM_WIN:
94 94 return sha256(str_).hexdigest()
95 elif __platform__ in ('Linux', 'Darwin'):
95 elif __platform__ in PLATFORM_OTHERS:
96 96 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
97 97 else:
98 raise Exception('Unknown or unsupoprted platform %s' % __platform__)
98 raise Exception('Unknown or unsupported platform %s' % __platform__)
99 99
100 100 @classmethod
101 101 def hash_check(cls, password, hashed):
102 102 """
103 103 Checks matching password with it's hashed value, runs different
104 104 implementation based on platform it runs on
105 105
106 106 :param password: password
107 107 :param hashed: password in hashed form
108 108 """
109 109
110 if __platform__ == 'Windows':
110 if __platform__ in PLATFORM_WIN:
111 111 return sha256(password).hexdigest() == hashed
112 elif __platform__ in ('Linux', 'Darwin'):
112 elif __platform__ in PLATFORM_OTHERS:
113 113 return bcrypt.hashpw(password, hashed) == hashed
114 114 else:
115 raise Exception('Unknown or unsupoprted platform %s' % __platform__)
116
117
118
115 raise Exception('Unknown or unsupported platform %s' % __platform__)
119 116
120 117
121 118 def get_crypt_password(password):
122 119 return RhodeCodeCrypto.hash_string(password)
123 120
124 121 def check_password(password, hashed):
125 122 return RhodeCodeCrypto.hash_check(password, hashed)
126 123
127 124 def generate_api_key(username, salt=None):
128 125 if salt is None:
129 126 salt = _RandomNameSequence().next()
130 127
131 128 return hashlib.sha1(username + salt).hexdigest()
132 129
133 130 def authfunc(environ, username, password):
134 131 """Dummy authentication function used in Mercurial/Git/ and access control,
135 132
136 133 :param environ: needed only for using in Basic auth
137 134 """
138 135 return authenticate(username, password)
139 136
140 137
141 138 def authenticate(username, password):
142 139 """Authentication function used for access control,
143 140 firstly checks for db authentication then if ldap is enabled for ldap
144 141 authentication, also creates ldap user if not in database
145 142
146 143 :param username: username
147 144 :param password: password
148 145 """
149 146 user_model = UserModel()
150 147 user = user_model.get_by_username(username, cache=False)
151 148
152 149 log.debug('Authenticating user using RhodeCode account')
153 150 if user is not None and not user.ldap_dn:
154 151 if user.active:
155 152
156 153 if user.username == 'default' and user.active:
157 154 log.info('user %s authenticated correctly as anonymous user',
158 155 username)
159 156 return True
160 157
161 158 elif user.username == username and check_password(password, user.password):
162 159 log.info('user %s authenticated correctly', username)
163 160 return True
164 161 else:
165 162 log.warning('user %s is disabled', username)
166 163
167 164 else:
168 165 log.debug('Regular authentication failed')
169 166 user_obj = user_model.get_by_username(username, cache=False,
170 167 case_insensitive=True)
171 168
172 169 if user_obj is not None and not user_obj.ldap_dn:
173 170 log.debug('this user already exists as non ldap')
174 171 return False
175 172
176 173 from rhodecode.model.settings import SettingsModel
177 174 ldap_settings = SettingsModel().get_ldap_settings()
178 175
179 176 #======================================================================
180 177 # FALLBACK TO LDAP AUTH IF ENABLE
181 178 #======================================================================
182 179 if str2bool(ldap_settings.get('ldap_active')):
183 180 log.debug("Authenticating user using ldap")
184 181 kwargs = {
185 182 'server':ldap_settings.get('ldap_host', ''),
186 183 'base_dn':ldap_settings.get('ldap_base_dn', ''),
187 184 'port':ldap_settings.get('ldap_port'),
188 185 'bind_dn':ldap_settings.get('ldap_dn_user'),
189 186 'bind_pass':ldap_settings.get('ldap_dn_pass'),
190 187 'use_ldaps':str2bool(ldap_settings.get('ldap_ldaps')),
191 188 'tls_reqcert':ldap_settings.get('ldap_tls_reqcert'),
192 189 'ldap_filter':ldap_settings.get('ldap_filter'),
193 190 'search_scope':ldap_settings.get('ldap_search_scope'),
194 191 'attr_login':ldap_settings.get('ldap_attr_login'),
195 192 'ldap_version':3,
196 193 }
197 194 log.debug('Checking for ldap authentication')
198 195 try:
199 196 aldap = AuthLdap(**kwargs)
200 197 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
201 198 log.debug('Got ldap DN response %s', user_dn)
202 199
203 200 user_attrs = {
204 201 'name' : ldap_attrs[ldap_settings.get('ldap_attr_firstname')][0],
205 202 'lastname' : ldap_attrs[ldap_settings.get('ldap_attr_lastname')][0],
206 203 'email' : ldap_attrs[ldap_settings.get('ldap_attr_email')][0],
207 204 }
208 205
209 206 if user_model.create_ldap(username, password, user_dn, user_attrs):
210 207 log.info('created new ldap user %s', username)
211 208
212 209 return True
213 210 except (LdapUsernameError, LdapPasswordError,):
214 211 pass
215 212 except (Exception,):
216 213 log.error(traceback.format_exc())
217 214 pass
218 215 return False
219 216
220 217 class AuthUser(object):
221 218 """
222 219 A simple object that handles all attributes of user in RhodeCode
223 220
224 221 It does lookup based on API key,given user, or user present in session
225 222 Then it fills all required information for such user. It also checks if
226 223 anonymous access is enabled and if so, it returns default user as logged
227 224 in
228 225 """
229 226
230 227 def __init__(self, user_id=None, api_key=None):
231 228
232 229 self.user_id = user_id
233 230 self.api_key = None
234 231
235 232 self.username = 'None'
236 233 self.name = ''
237 234 self.lastname = ''
238 235 self.email = ''
239 236 self.is_authenticated = False
240 237 self.admin = False
241 238 self.permissions = {}
242 239 self._api_key = api_key
243 240 self.propagate_data()
244 241
245 242
246 243 def propagate_data(self):
247 244 user_model = UserModel()
248 245 self.anonymous_user = user_model.get_by_username('default', cache=True)
249 246 if self._api_key and self._api_key != self.anonymous_user.api_key:
250 247 #try go get user by api key
251 248 log.debug('Auth User lookup by API KEY %s', self._api_key)
252 249 user_model.fill_data(self, api_key=self._api_key)
253 250 else:
254 251 log.debug('Auth User lookup by USER ID %s', self.user_id)
255 252 if self.user_id is not None and self.user_id != self.anonymous_user.user_id:
256 253 user_model.fill_data(self, user_id=self.user_id)
257 254 else:
258 255 if self.anonymous_user.active is True:
259 256 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
260 257 #then we set this user is logged in
261 258 self.is_authenticated = True
262 259 else:
263 260 self.is_authenticated = False
264 261
265 262 log.debug('Auth User is now %s', self)
266 263 user_model.fill_perms(self)
267 264
268 265 @property
269 266 def is_admin(self):
270 267 return self.admin
271 268
272 269 def __repr__(self):
273 270 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
274 271 self.is_authenticated)
275 272
276 273 def set_authenticated(self, authenticated=True):
277 274
278 275 if self.user_id != self.anonymous_user.user_id:
279 276 self.is_authenticated = authenticated
280 277
281 278
282 279 def set_available_permissions(config):
283 280 """This function will propagate pylons globals with all available defined
284 281 permission given in db. We don't want to check each time from db for new
285 282 permissions since adding a new permission also requires application restart
286 283 ie. to decorate new views with the newly created permission
287 284
288 285 :param config: current pylons config instance
289 286
290 287 """
291 288 log.info('getting information about all available permissions')
292 289 try:
293 290 sa = meta.Session()
294 291 all_perms = sa.query(Permission).all()
295 292 except:
296 293 pass
297 294 finally:
298 295 meta.Session.remove()
299 296
300 297 config['available_permissions'] = [x.permission_name for x in all_perms]
301 298
302 299 #===============================================================================
303 300 # CHECK DECORATORS
304 301 #===============================================================================
305 302 class LoginRequired(object):
306 303 """
307 304 Must be logged in to execute this function else
308 305 redirect to login page
309 306
310 307 :param api_access: if enabled this checks only for valid auth token
311 308 and grants access based on valid token
312 309 """
313 310
314 311 def __init__(self, api_access=False):
315 312 self.api_access = api_access
316 313
317 314 def __call__(self, func):
318 315 return decorator(self.__wrapper, func)
319 316
320 317 def __wrapper(self, func, *fargs, **fkwargs):
321 318 cls = fargs[0]
322 319 user = cls.rhodecode_user
323 320
324 321 api_access_ok = False
325 322 if self.api_access:
326 323 log.debug('Checking API KEY access for %s', cls)
327 324 if user.api_key == request.GET.get('api_key'):
328 325 api_access_ok = True
329 326 else:
330 327 log.debug("API KEY token not valid")
331 328
332 329 log.debug('Checking if %s is authenticated @ %s', user.username, cls)
333 330 if user.is_authenticated or api_access_ok:
334 331 log.debug('user %s is authenticated', user.username)
335 332 return func(*fargs, **fkwargs)
336 333 else:
337 334 log.warn('user %s NOT authenticated', user.username)
338 335
339 336 p = ''
340 337 if request.environ.get('SCRIPT_NAME') != '/':
341 338 p += request.environ.get('SCRIPT_NAME')
342 339
343 340 p += request.environ.get('PATH_INFO')
344 341 if request.environ.get('QUERY_STRING'):
345 342 p += '?' + request.environ.get('QUERY_STRING')
346 343
347 344 log.debug('redirecting to login page with %s', p)
348 345 return redirect(url('login_home', came_from=p))
349 346
350 347 class NotAnonymous(object):
351 348 """Must be logged in to execute this function else
352 349 redirect to login page"""
353 350
354 351 def __call__(self, func):
355 352 return decorator(self.__wrapper, func)
356 353
357 354 def __wrapper(self, func, *fargs, **fkwargs):
358 355 cls = fargs[0]
359 356 self.user = cls.rhodecode_user
360 357
361 358 log.debug('Checking if user is not anonymous @%s', cls)
362 359
363 360 anonymous = self.user.username == 'default'
364 361
365 362 if anonymous:
366 363 p = ''
367 364 if request.environ.get('SCRIPT_NAME') != '/':
368 365 p += request.environ.get('SCRIPT_NAME')
369 366
370 367 p += request.environ.get('PATH_INFO')
371 368 if request.environ.get('QUERY_STRING'):
372 369 p += '?' + request.environ.get('QUERY_STRING')
373 370
374 371 import rhodecode.lib.helpers as h
375 372 h.flash(_('You need to be a registered user to perform this action'),
376 373 category='warning')
377 374 return redirect(url('login_home', came_from=p))
378 375 else:
379 376 return func(*fargs, **fkwargs)
380 377
381 378 class PermsDecorator(object):
382 379 """Base class for controller decorators"""
383 380
384 381 def __init__(self, *required_perms):
385 382 available_perms = config['available_permissions']
386 383 for perm in required_perms:
387 384 if perm not in available_perms:
388 385 raise Exception("'%s' permission is not defined" % perm)
389 386 self.required_perms = set(required_perms)
390 387 self.user_perms = None
391 388
392 389 def __call__(self, func):
393 390 return decorator(self.__wrapper, func)
394 391
395 392
396 393 def __wrapper(self, func, *fargs, **fkwargs):
397 394 cls = fargs[0]
398 395 self.user = cls.rhodecode_user
399 396 self.user_perms = self.user.permissions
400 397 log.debug('checking %s permissions %s for %s %s',
401 398 self.__class__.__name__, self.required_perms, cls,
402 399 self.user)
403 400
404 401 if self.check_permissions():
405 402 log.debug('Permission granted for %s %s', cls, self.user)
406 403 return func(*fargs, **fkwargs)
407 404
408 405 else:
409 406 log.warning('Permission denied for %s %s', cls, self.user)
410 407 #redirect with forbidden ret code
411 408 return abort(403)
412 409
413 410
414 411
415 412 def check_permissions(self):
416 413 """Dummy function for overriding"""
417 414 raise Exception('You have to write this function in child class')
418 415
419 416 class HasPermissionAllDecorator(PermsDecorator):
420 417 """Checks for access permission for all given predicates. All of them
421 418 have to be meet in order to fulfill the request
422 419 """
423 420
424 421 def check_permissions(self):
425 422 if self.required_perms.issubset(self.user_perms.get('global')):
426 423 return True
427 424 return False
428 425
429 426
430 427 class HasPermissionAnyDecorator(PermsDecorator):
431 428 """Checks for access permission for any of given predicates. In order to
432 429 fulfill the request any of predicates must be meet
433 430 """
434 431
435 432 def check_permissions(self):
436 433 if self.required_perms.intersection(self.user_perms.get('global')):
437 434 return True
438 435 return False
439 436
440 437 class HasRepoPermissionAllDecorator(PermsDecorator):
441 438 """Checks for access permission for all given predicates for specific
442 439 repository. All of them have to be meet in order to fulfill the request
443 440 """
444 441
445 442 def check_permissions(self):
446 443 repo_name = get_repo_slug(request)
447 444 try:
448 445 user_perms = set([self.user_perms['repositories'][repo_name]])
449 446 except KeyError:
450 447 return False
451 448 if self.required_perms.issubset(user_perms):
452 449 return True
453 450 return False
454 451
455 452
456 453 class HasRepoPermissionAnyDecorator(PermsDecorator):
457 454 """Checks for access permission for any of given predicates for specific
458 455 repository. In order to fulfill the request any of predicates must be meet
459 456 """
460 457
461 458 def check_permissions(self):
462 459 repo_name = get_repo_slug(request)
463 460
464 461 try:
465 462 user_perms = set([self.user_perms['repositories'][repo_name]])
466 463 except KeyError:
467 464 return False
468 465 if self.required_perms.intersection(user_perms):
469 466 return True
470 467 return False
471 468 #===============================================================================
472 469 # CHECK FUNCTIONS
473 470 #===============================================================================
474 471
475 472 class PermsFunction(object):
476 473 """Base function for other check functions"""
477 474
478 475 def __init__(self, *perms):
479 476 available_perms = config['available_permissions']
480 477
481 478 for perm in perms:
482 479 if perm not in available_perms:
483 480 raise Exception("'%s' permission in not defined" % perm)
484 481 self.required_perms = set(perms)
485 482 self.user_perms = None
486 483 self.granted_for = ''
487 484 self.repo_name = None
488 485
489 486 def __call__(self, check_Location=''):
490 487 user = session.get('rhodecode_user', False)
491 488 if not user:
492 489 return False
493 490 self.user_perms = user.permissions
494 491 self.granted_for = user
495 492 log.debug('checking %s %s %s', self.__class__.__name__,
496 493 self.required_perms, user)
497 494
498 495 if self.check_permissions():
499 496 log.debug('Permission granted %s @ %s', self.granted_for,
500 497 check_Location or 'unspecified location')
501 498 return True
502 499
503 500 else:
504 501 log.warning('Permission denied for %s @ %s', self.granted_for,
505 502 check_Location or 'unspecified location')
506 503 return False
507 504
508 505 def check_permissions(self):
509 506 """Dummy function for overriding"""
510 507 raise Exception('You have to write this function in child class')
511 508
512 509 class HasPermissionAll(PermsFunction):
513 510 def check_permissions(self):
514 511 if self.required_perms.issubset(self.user_perms.get('global')):
515 512 return True
516 513 return False
517 514
518 515 class HasPermissionAny(PermsFunction):
519 516 def check_permissions(self):
520 517 if self.required_perms.intersection(self.user_perms.get('global')):
521 518 return True
522 519 return False
523 520
524 521 class HasRepoPermissionAll(PermsFunction):
525 522
526 523 def __call__(self, repo_name=None, check_Location=''):
527 524 self.repo_name = repo_name
528 525 return super(HasRepoPermissionAll, self).__call__(check_Location)
529 526
530 527 def check_permissions(self):
531 528 if not self.repo_name:
532 529 self.repo_name = get_repo_slug(request)
533 530
534 531 try:
535 532 self.user_perms = set([self.user_perms['repositories']\
536 533 [self.repo_name]])
537 534 except KeyError:
538 535 return False
539 536 self.granted_for = self.repo_name
540 537 if self.required_perms.issubset(self.user_perms):
541 538 return True
542 539 return False
543 540
544 541 class HasRepoPermissionAny(PermsFunction):
545 542
546 543 def __call__(self, repo_name=None, check_Location=''):
547 544 self.repo_name = repo_name
548 545 return super(HasRepoPermissionAny, self).__call__(check_Location)
549 546
550 547 def check_permissions(self):
551 548 if not self.repo_name:
552 549 self.repo_name = get_repo_slug(request)
553 550
554 551 try:
555 552 self.user_perms = set([self.user_perms['repositories']\
556 553 [self.repo_name]])
557 554 except KeyError:
558 555 return False
559 556 self.granted_for = self.repo_name
560 557 if self.required_perms.intersection(self.user_perms):
561 558 return True
562 559 return False
563 560
564 561 #===============================================================================
565 562 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
566 563 #===============================================================================
567 564
568 565 class HasPermissionAnyMiddleware(object):
569 566 def __init__(self, *perms):
570 567 self.required_perms = set(perms)
571 568
572 569 def __call__(self, user, repo_name):
573 570 usr = AuthUser(user.user_id)
574 571 try:
575 572 self.user_perms = set([usr.permissions['repositories'][repo_name]])
576 573 except:
577 574 self.user_perms = set()
578 575 self.granted_for = ''
579 576 self.username = user.username
580 577 self.repo_name = repo_name
581 578 return self.check_permissions()
582 579
583 580 def check_permissions(self):
584 581 log.debug('checking mercurial protocol '
585 582 'permissions %s for user:%s repository:%s', self.user_perms,
586 583 self.username, self.repo_name)
587 584 if self.required_perms.intersection(self.user_perms):
588 585 log.debug('permission granted')
589 586 return True
590 587 log.debug('permission denied')
591 588 return False
General Comments 0
You need to be logged in to leave comments. Login now