##// END OF EJS Templates
ldap: handle more elegantly that python-ldap isn't installed when trying to use ldap...
Mads Kiilerich -
r3632:1ec67ddc beta
parent child Browse files
Show More
@@ -1,1029 +1,1030 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 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import random
27 27 import logging
28 28 import traceback
29 29 import hashlib
30 30
31 31 from tempfile import _RandomNameSequence
32 32 from decorator import decorator
33 33
34 34 from pylons import config, url, request
35 35 from pylons.controllers.util import abort, redirect
36 36 from pylons.i18n.translation import _
37 37 from sqlalchemy.orm.exc import ObjectDeletedError
38 38
39 39 from rhodecode import __platform__, is_windows, is_unix
40 40 from rhodecode.model.meta import Session
41 41
42 42 from rhodecode.lib.utils2 import str2bool, safe_unicode
43 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
43 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError,\
44 LdapImportError
44 45 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
45 46 from rhodecode.lib.auth_ldap import AuthLdap
46 47
47 48 from rhodecode.model import meta
48 49 from rhodecode.model.user import UserModel
49 50 from rhodecode.model.db import Permission, RhodeCodeSetting, User, UserIpMap
50 51 from rhodecode.lib.caching_query import FromCache
51 52
52 53 log = logging.getLogger(__name__)
53 54
54 55
55 56 class PasswordGenerator(object):
56 57 """
57 58 This is a simple class for generating password from different sets of
58 59 characters
59 60 usage::
60 61
61 62 passwd_gen = PasswordGenerator()
62 63 #print 8-letter password containing only big and small letters
63 64 of alphabet
64 65 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
65 66 """
66 67 ALPHABETS_NUM = r'''1234567890'''
67 68 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
68 69 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
69 70 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
70 71 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
71 72 + ALPHABETS_NUM + ALPHABETS_SPECIAL
72 73 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
73 74 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
74 75 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
75 76 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
76 77
77 78 def __init__(self, passwd=''):
78 79 self.passwd = passwd
79 80
80 81 def gen_password(self, length, type_=None):
81 82 if type_ is None:
82 83 type_ = self.ALPHABETS_FULL
83 84 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
84 85 return self.passwd
85 86
86 87
87 88 class RhodeCodeCrypto(object):
88 89
89 90 @classmethod
90 91 def hash_string(cls, str_):
91 92 """
92 93 Cryptographic function used for password hashing based on pybcrypt
93 94 or pycrypto in windows
94 95
95 96 :param password: password to hash
96 97 """
97 98 if is_windows:
98 99 from hashlib import sha256
99 100 return sha256(str_).hexdigest()
100 101 elif is_unix:
101 102 import bcrypt
102 103 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
103 104 else:
104 105 raise Exception('Unknown or unsupported platform %s' \
105 106 % __platform__)
106 107
107 108 @classmethod
108 109 def hash_check(cls, password, hashed):
109 110 """
110 111 Checks matching password with it's hashed value, runs different
111 112 implementation based on platform it runs on
112 113
113 114 :param password: password
114 115 :param hashed: password in hashed form
115 116 """
116 117
117 118 if is_windows:
118 119 from hashlib import sha256
119 120 return sha256(password).hexdigest() == hashed
120 121 elif is_unix:
121 122 import bcrypt
122 123 return bcrypt.hashpw(password, hashed) == hashed
123 124 else:
124 125 raise Exception('Unknown or unsupported platform %s' \
125 126 % __platform__)
126 127
127 128
128 129 def get_crypt_password(password):
129 130 return RhodeCodeCrypto.hash_string(password)
130 131
131 132
132 133 def check_password(password, hashed):
133 134 return RhodeCodeCrypto.hash_check(password, hashed)
134 135
135 136
136 137 def generate_api_key(str_, salt=None):
137 138 """
138 139 Generates API KEY from given string
139 140
140 141 :param str_:
141 142 :param salt:
142 143 """
143 144
144 145 if salt is None:
145 146 salt = _RandomNameSequence().next()
146 147
147 148 return hashlib.sha1(str_ + salt).hexdigest()
148 149
149 150
150 151 def authfunc(environ, username, password):
151 152 """
152 153 Dummy authentication wrapper function used in Mercurial and Git for
153 154 access control.
154 155
155 156 :param environ: needed only for using in Basic auth
156 157 """
157 158 return authenticate(username, password)
158 159
159 160
160 161 def authenticate(username, password):
161 162 """
162 163 Authentication function used for access control,
163 164 firstly checks for db authentication then if ldap is enabled for ldap
164 165 authentication, also creates ldap user if not in database
165 166
166 167 :param username: username
167 168 :param password: password
168 169 """
169 170
170 171 user_model = UserModel()
171 172 user = User.get_by_username(username)
172 173
173 174 log.debug('Authenticating user using RhodeCode account')
174 175 if user is not None and not user.ldap_dn:
175 176 if user.active:
176 177 if user.username == 'default' and user.active:
177 178 log.info('user %s authenticated correctly as anonymous user' %
178 179 username)
179 180 return True
180 181
181 182 elif user.username == username and check_password(password,
182 183 user.password):
183 184 log.info('user %s authenticated correctly' % username)
184 185 return True
185 186 else:
186 187 log.warning('user %s tried auth but is disabled' % username)
187 188
188 189 else:
189 190 log.debug('Regular authentication failed')
190 191 user_obj = User.get_by_username(username, case_insensitive=True)
191 192
192 193 if user_obj is not None and not user_obj.ldap_dn:
193 194 log.debug('this user already exists as non ldap')
194 195 return False
195 196
196 197 ldap_settings = RhodeCodeSetting.get_ldap_settings()
197 198 #======================================================================
198 199 # FALLBACK TO LDAP AUTH IF ENABLE
199 200 #======================================================================
200 201 if str2bool(ldap_settings.get('ldap_active')):
201 202 log.debug("Authenticating user using ldap")
202 203 kwargs = {
203 204 'server': ldap_settings.get('ldap_host', ''),
204 205 'base_dn': ldap_settings.get('ldap_base_dn', ''),
205 206 'port': ldap_settings.get('ldap_port'),
206 207 'bind_dn': ldap_settings.get('ldap_dn_user'),
207 208 'bind_pass': ldap_settings.get('ldap_dn_pass'),
208 209 'tls_kind': ldap_settings.get('ldap_tls_kind'),
209 210 'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
210 211 'ldap_filter': ldap_settings.get('ldap_filter'),
211 212 'search_scope': ldap_settings.get('ldap_search_scope'),
212 213 'attr_login': ldap_settings.get('ldap_attr_login'),
213 214 'ldap_version': 3,
214 215 }
215 216 log.debug('Checking for ldap authentication')
216 217 try:
217 218 aldap = AuthLdap(**kwargs)
218 219 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
219 220 password)
220 221 log.debug('Got ldap DN response %s' % user_dn)
221 222
222 223 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
223 224 .get(k), [''])[0]
224 225
225 226 user_attrs = {
226 227 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
227 228 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
228 229 'email': get_ldap_attr('ldap_attr_email'),
229 230 'active': 'hg.register.auto_activate' in User\
230 231 .get_by_username('default').AuthUser.permissions['global']
231 232 }
232 233
233 234 # don't store LDAP password since we don't need it. Override
234 235 # with some random generated password
235 236 _password = PasswordGenerator().gen_password(length=8)
236 237 # create this user on the fly if it doesn't exist in rhodecode
237 238 # database
238 239 if user_model.create_ldap(username, _password, user_dn,
239 240 user_attrs):
240 241 log.info('created new ldap user %s' % username)
241 242
242 243 Session().commit()
243 244 return True
244 except (LdapUsernameError, LdapPasswordError,):
245 except (LdapUsernameError, LdapPasswordError, LdapImportError):
245 246 pass
246 247 except (Exception,):
247 248 log.error(traceback.format_exc())
248 249 pass
249 250 return False
250 251
251 252
252 253 def login_container_auth(username):
253 254 user = User.get_by_username(username)
254 255 if user is None:
255 256 user_attrs = {
256 257 'name': username,
257 258 'lastname': None,
258 259 'email': None,
259 260 'active': 'hg.register.auto_activate' in User\
260 261 .get_by_username('default').AuthUser.permissions['global']
261 262 }
262 263 user = UserModel().create_for_container_auth(username, user_attrs)
263 264 if not user:
264 265 return None
265 266 log.info('User %s was created by container authentication' % username)
266 267
267 268 if not user.active:
268 269 return None
269 270
270 271 user.update_lastlogin()
271 272 Session().commit()
272 273
273 274 log.debug('User %s is now logged in by container authentication',
274 275 user.username)
275 276 return user
276 277
277 278
278 279 def get_container_username(environ, config, clean_username=False):
279 280 """
280 281 Get's the container_auth username (or email). It tries to get username
281 282 from REMOTE_USER if container_auth_enabled is enabled, if that fails
282 283 it tries to get username from HTTP_X_FORWARDED_USER if proxypass_auth_enabled
283 284 is enabled. clean_username extracts the username from this data if it's
284 285 having @ in it.
285 286
286 287 :param environ:
287 288 :param config:
288 289 :param clean_username:
289 290 """
290 291 username = None
291 292
292 293 if str2bool(config.get('container_auth_enabled', False)):
293 294 from paste.httpheaders import REMOTE_USER
294 295 username = REMOTE_USER(environ)
295 296 log.debug('extracted REMOTE_USER:%s' % (username))
296 297
297 298 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
298 299 username = environ.get('HTTP_X_FORWARDED_USER')
299 300 log.debug('extracted HTTP_X_FORWARDED_USER:%s' % (username))
300 301
301 302 if username and clean_username:
302 303 # Removing realm and domain from username
303 304 username = username.partition('@')[0]
304 305 username = username.rpartition('\\')[2]
305 306 log.debug('Received username %s from container' % username)
306 307
307 308 return username
308 309
309 310
310 311 class CookieStoreWrapper(object):
311 312
312 313 def __init__(self, cookie_store):
313 314 self.cookie_store = cookie_store
314 315
315 316 def __repr__(self):
316 317 return 'CookieStore<%s>' % (self.cookie_store)
317 318
318 319 def get(self, key, other=None):
319 320 if isinstance(self.cookie_store, dict):
320 321 return self.cookie_store.get(key, other)
321 322 elif isinstance(self.cookie_store, AuthUser):
322 323 return self.cookie_store.__dict__.get(key, other)
323 324
324 325
325 326 class AuthUser(object):
326 327 """
327 328 A simple object that handles all attributes of user in RhodeCode
328 329
329 330 It does lookup based on API key,given user, or user present in session
330 331 Then it fills all required information for such user. It also checks if
331 332 anonymous access is enabled and if so, it returns default user as logged
332 333 in
333 334 """
334 335
335 336 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
336 337
337 338 self.user_id = user_id
338 339 self.api_key = None
339 340 self.username = username
340 341 self.ip_addr = ip_addr
341 342
342 343 self.name = ''
343 344 self.lastname = ''
344 345 self.email = ''
345 346 self.is_authenticated = False
346 347 self.admin = False
347 348 self.inherit_default_permissions = False
348 349 self.permissions = {}
349 350 self._api_key = api_key
350 351 self.propagate_data()
351 352 self._instance = None
352 353
353 354 def propagate_data(self):
354 355 user_model = UserModel()
355 356 self.anonymous_user = User.get_by_username('default', cache=True)
356 357 is_user_loaded = False
357 358
358 359 # try go get user by api key
359 360 if self._api_key and self._api_key != self.anonymous_user.api_key:
360 361 log.debug('Auth User lookup by API KEY %s' % self._api_key)
361 362 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
362 363 # lookup by userid
363 364 elif (self.user_id is not None and
364 365 self.user_id != self.anonymous_user.user_id):
365 366 log.debug('Auth User lookup by USER ID %s' % self.user_id)
366 367 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
367 368 # lookup by username
368 369 elif self.username and \
369 370 str2bool(config.get('container_auth_enabled', False)):
370 371
371 372 log.debug('Auth User lookup by USER NAME %s' % self.username)
372 373 dbuser = login_container_auth(self.username)
373 374 if dbuser is not None:
374 375 log.debug('filling all attributes to object')
375 376 for k, v in dbuser.get_dict().items():
376 377 setattr(self, k, v)
377 378 self.set_authenticated()
378 379 is_user_loaded = True
379 380 else:
380 381 log.debug('No data in %s that could been used to log in' % self)
381 382
382 383 if not is_user_loaded:
383 384 # if we cannot authenticate user try anonymous
384 385 if self.anonymous_user.active:
385 386 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
386 387 # then we set this user is logged in
387 388 self.is_authenticated = True
388 389 else:
389 390 self.user_id = None
390 391 self.username = None
391 392 self.is_authenticated = False
392 393
393 394 if not self.username:
394 395 self.username = 'None'
395 396
396 397 log.debug('Auth User is now %s' % self)
397 398 user_model.fill_perms(self)
398 399
399 400 @property
400 401 def is_admin(self):
401 402 return self.admin
402 403
403 404 @property
404 405 def repos_admin(self):
405 406 """
406 407 Returns list of repositories you're an admin of
407 408 """
408 409 return [x[0] for x in self.permissions['repositories'].iteritems()
409 410 if x[1] == 'repository.admin']
410 411
411 412 @property
412 413 def groups_admin(self):
413 414 """
414 415 Returns list of repository groups you're an admin of
415 416 """
416 417 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
417 418 if x[1] == 'group.admin']
418 419
419 420 @property
420 421 def ip_allowed(self):
421 422 """
422 423 Checks if ip_addr used in constructor is allowed from defined list of
423 424 allowed ip_addresses for user
424 425
425 426 :returns: boolean, True if ip is in allowed ip range
426 427 """
427 428 #check IP
428 429 allowed_ips = AuthUser.get_allowed_ips(self.user_id, cache=True)
429 430 if check_ip_access(source_ip=self.ip_addr, allowed_ips=allowed_ips):
430 431 log.debug('IP:%s is in range of %s' % (self.ip_addr, allowed_ips))
431 432 return True
432 433 else:
433 434 log.info('Access for IP:%s forbidden, '
434 435 'not in %s' % (self.ip_addr, allowed_ips))
435 436 return False
436 437
437 438 def __repr__(self):
438 439 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
439 440 self.is_authenticated)
440 441
441 442 def set_authenticated(self, authenticated=True):
442 443 if self.user_id != self.anonymous_user.user_id:
443 444 self.is_authenticated = authenticated
444 445
445 446 def get_cookie_store(self):
446 447 return {'username': self.username,
447 448 'user_id': self.user_id,
448 449 'is_authenticated': self.is_authenticated}
449 450
450 451 @classmethod
451 452 def from_cookie_store(cls, cookie_store):
452 453 """
453 454 Creates AuthUser from a cookie store
454 455
455 456 :param cls:
456 457 :param cookie_store:
457 458 """
458 459 user_id = cookie_store.get('user_id')
459 460 username = cookie_store.get('username')
460 461 api_key = cookie_store.get('api_key')
461 462 return AuthUser(user_id, api_key, username)
462 463
463 464 @classmethod
464 465 def get_allowed_ips(cls, user_id, cache=False):
465 466 _set = set()
466 467 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
467 468 if cache:
468 469 user_ips = user_ips.options(FromCache("sql_cache_short",
469 470 "get_user_ips_%s" % user_id))
470 471 for ip in user_ips:
471 472 try:
472 473 _set.add(ip.ip_addr)
473 474 except ObjectDeletedError:
474 475 # since we use heavy caching sometimes it happens that we get
475 476 # deleted objects here, we just skip them
476 477 pass
477 478 return _set or set(['0.0.0.0/0', '::/0'])
478 479
479 480
480 481 def set_available_permissions(config):
481 482 """
482 483 This function will propagate pylons globals with all available defined
483 484 permission given in db. We don't want to check each time from db for new
484 485 permissions since adding a new permission also requires application restart
485 486 ie. to decorate new views with the newly created permission
486 487
487 488 :param config: current pylons config instance
488 489
489 490 """
490 491 log.info('getting information about all available permissions')
491 492 try:
492 493 sa = meta.Session
493 494 all_perms = sa.query(Permission).all()
494 495 except Exception:
495 496 pass
496 497 finally:
497 498 meta.Session.remove()
498 499
499 500 config['available_permissions'] = [x.permission_name for x in all_perms]
500 501
501 502
502 503 #==============================================================================
503 504 # CHECK DECORATORS
504 505 #==============================================================================
505 506 class LoginRequired(object):
506 507 """
507 508 Must be logged in to execute this function else
508 509 redirect to login page
509 510
510 511 :param api_access: if enabled this checks only for valid auth token
511 512 and grants access based on valid token
512 513 """
513 514
514 515 def __init__(self, api_access=False):
515 516 self.api_access = api_access
516 517
517 518 def __call__(self, func):
518 519 return decorator(self.__wrapper, func)
519 520
520 521 def __wrapper(self, func, *fargs, **fkwargs):
521 522 cls = fargs[0]
522 523 user = cls.rhodecode_user
523 524 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
524 525
525 526 #check IP
526 527 ip_access_ok = True
527 528 if not user.ip_allowed:
528 529 from rhodecode.lib import helpers as h
529 530 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr))),
530 531 category='warning')
531 532 ip_access_ok = False
532 533
533 534 api_access_ok = False
534 535 if self.api_access:
535 536 log.debug('Checking API KEY access for %s' % cls)
536 537 if user.api_key == request.GET.get('api_key'):
537 538 api_access_ok = True
538 539 else:
539 540 log.debug("API KEY token not valid")
540 541
541 542 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
542 543 if (user.is_authenticated or api_access_ok) and ip_access_ok:
543 544 reason = 'RegularAuth' if user.is_authenticated else 'APIAuth'
544 545 log.info('user %s is authenticated and granted access to %s '
545 546 'using %s' % (user.username, loc, reason)
546 547 )
547 548 return func(*fargs, **fkwargs)
548 549 else:
549 550 log.warn('user %s NOT authenticated on func: %s' % (
550 551 user, loc)
551 552 )
552 553 p = url.current()
553 554
554 555 log.debug('redirecting to login page with %s' % p)
555 556 return redirect(url('login_home', came_from=p))
556 557
557 558
558 559 class NotAnonymous(object):
559 560 """
560 561 Must be logged in to execute this function else
561 562 redirect to login page"""
562 563
563 564 def __call__(self, func):
564 565 return decorator(self.__wrapper, func)
565 566
566 567 def __wrapper(self, func, *fargs, **fkwargs):
567 568 cls = fargs[0]
568 569 self.user = cls.rhodecode_user
569 570
570 571 log.debug('Checking if user is not anonymous @%s' % cls)
571 572
572 573 anonymous = self.user.username == 'default'
573 574
574 575 if anonymous:
575 576 p = url.current()
576 577
577 578 import rhodecode.lib.helpers as h
578 579 h.flash(_('You need to be a registered user to '
579 580 'perform this action'),
580 581 category='warning')
581 582 return redirect(url('login_home', came_from=p))
582 583 else:
583 584 return func(*fargs, **fkwargs)
584 585
585 586
586 587 class PermsDecorator(object):
587 588 """Base class for controller decorators"""
588 589
589 590 def __init__(self, *required_perms):
590 591 available_perms = config['available_permissions']
591 592 for perm in required_perms:
592 593 if perm not in available_perms:
593 594 raise Exception("'%s' permission is not defined" % perm)
594 595 self.required_perms = set(required_perms)
595 596 self.user_perms = None
596 597
597 598 def __call__(self, func):
598 599 return decorator(self.__wrapper, func)
599 600
600 601 def __wrapper(self, func, *fargs, **fkwargs):
601 602 cls = fargs[0]
602 603 self.user = cls.rhodecode_user
603 604 self.user_perms = self.user.permissions
604 605 log.debug('checking %s permissions %s for %s %s',
605 606 self.__class__.__name__, self.required_perms, cls, self.user)
606 607
607 608 if self.check_permissions():
608 609 log.debug('Permission granted for %s %s' % (cls, self.user))
609 610 return func(*fargs, **fkwargs)
610 611
611 612 else:
612 613 log.debug('Permission denied for %s %s' % (cls, self.user))
613 614 anonymous = self.user.username == 'default'
614 615
615 616 if anonymous:
616 617 p = url.current()
617 618
618 619 import rhodecode.lib.helpers as h
619 620 h.flash(_('You need to be a signed in to '
620 621 'view this page'),
621 622 category='warning')
622 623 return redirect(url('login_home', came_from=p))
623 624
624 625 else:
625 626 # redirect with forbidden ret code
626 627 return abort(403)
627 628
628 629 def check_permissions(self):
629 630 """Dummy function for overriding"""
630 631 raise Exception('You have to write this function in child class')
631 632
632 633
633 634 class HasPermissionAllDecorator(PermsDecorator):
634 635 """
635 636 Checks for access permission for all given predicates. All of them
636 637 have to be meet in order to fulfill the request
637 638 """
638 639
639 640 def check_permissions(self):
640 641 if self.required_perms.issubset(self.user_perms.get('global')):
641 642 return True
642 643 return False
643 644
644 645
645 646 class HasPermissionAnyDecorator(PermsDecorator):
646 647 """
647 648 Checks for access permission for any of given predicates. In order to
648 649 fulfill the request any of predicates must be meet
649 650 """
650 651
651 652 def check_permissions(self):
652 653 if self.required_perms.intersection(self.user_perms.get('global')):
653 654 return True
654 655 return False
655 656
656 657
657 658 class HasRepoPermissionAllDecorator(PermsDecorator):
658 659 """
659 660 Checks for access permission for all given predicates for specific
660 661 repository. All of them have to be meet in order to fulfill the request
661 662 """
662 663
663 664 def check_permissions(self):
664 665 repo_name = get_repo_slug(request)
665 666 try:
666 667 user_perms = set([self.user_perms['repositories'][repo_name]])
667 668 except KeyError:
668 669 return False
669 670 if self.required_perms.issubset(user_perms):
670 671 return True
671 672 return False
672 673
673 674
674 675 class HasRepoPermissionAnyDecorator(PermsDecorator):
675 676 """
676 677 Checks for access permission for any of given predicates for specific
677 678 repository. In order to fulfill the request any of predicates must be meet
678 679 """
679 680
680 681 def check_permissions(self):
681 682 repo_name = get_repo_slug(request)
682 683 try:
683 684 user_perms = set([self.user_perms['repositories'][repo_name]])
684 685 except KeyError:
685 686 return False
686 687
687 688 if self.required_perms.intersection(user_perms):
688 689 return True
689 690 return False
690 691
691 692
692 693 class HasReposGroupPermissionAllDecorator(PermsDecorator):
693 694 """
694 695 Checks for access permission for all given predicates for specific
695 696 repository. All of them have to be meet in order to fulfill the request
696 697 """
697 698
698 699 def check_permissions(self):
699 700 group_name = get_repos_group_slug(request)
700 701 try:
701 702 user_perms = set([self.user_perms['repositories_groups'][group_name]])
702 703 except KeyError:
703 704 return False
704 705
705 706 if self.required_perms.issubset(user_perms):
706 707 return True
707 708 return False
708 709
709 710
710 711 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
711 712 """
712 713 Checks for access permission for any of given predicates for specific
713 714 repository. In order to fulfill the request any of predicates must be meet
714 715 """
715 716
716 717 def check_permissions(self):
717 718 group_name = get_repos_group_slug(request)
718 719 try:
719 720 user_perms = set([self.user_perms['repositories_groups'][group_name]])
720 721 except KeyError:
721 722 return False
722 723
723 724 if self.required_perms.intersection(user_perms):
724 725 return True
725 726 return False
726 727
727 728
728 729 #==============================================================================
729 730 # CHECK FUNCTIONS
730 731 #==============================================================================
731 732 class PermsFunction(object):
732 733 """Base function for other check functions"""
733 734
734 735 def __init__(self, *perms):
735 736 available_perms = config['available_permissions']
736 737
737 738 for perm in perms:
738 739 if perm not in available_perms:
739 740 raise Exception("'%s' permission is not defined" % perm)
740 741 self.required_perms = set(perms)
741 742 self.user_perms = None
742 743 self.repo_name = None
743 744 self.group_name = None
744 745
745 746 def __call__(self, check_location=''):
746 747 #TODO: put user as attribute here
747 748 user = request.user
748 749 cls_name = self.__class__.__name__
749 750 check_scope = {
750 751 'HasPermissionAll': '',
751 752 'HasPermissionAny': '',
752 753 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
753 754 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
754 755 'HasReposGroupPermissionAll': 'group:%s' % self.group_name,
755 756 'HasReposGroupPermissionAny': 'group:%s' % self.group_name,
756 757 }.get(cls_name, '?')
757 758 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
758 759 self.required_perms, user, check_scope,
759 760 check_location or 'unspecified location')
760 761 if not user:
761 762 log.debug('Empty request user')
762 763 return False
763 764 self.user_perms = user.permissions
764 765 if self.check_permissions():
765 766 log.debug('Permission to %s granted for user: %s @ %s', self.repo_name, user,
766 767 check_location or 'unspecified location')
767 768 return True
768 769
769 770 else:
770 771 log.debug('Permission to %s denied for user: %s @ %s', self.repo_name, user,
771 772 check_location or 'unspecified location')
772 773 return False
773 774
774 775 def check_permissions(self):
775 776 """Dummy function for overriding"""
776 777 raise Exception('You have to write this function in child class')
777 778
778 779
779 780 class HasPermissionAll(PermsFunction):
780 781 def check_permissions(self):
781 782 if self.required_perms.issubset(self.user_perms.get('global')):
782 783 return True
783 784 return False
784 785
785 786
786 787 class HasPermissionAny(PermsFunction):
787 788 def check_permissions(self):
788 789 if self.required_perms.intersection(self.user_perms.get('global')):
789 790 return True
790 791 return False
791 792
792 793
793 794 class HasRepoPermissionAll(PermsFunction):
794 795 def __call__(self, repo_name=None, check_location=''):
795 796 self.repo_name = repo_name
796 797 return super(HasRepoPermissionAll, self).__call__(check_location)
797 798
798 799 def check_permissions(self):
799 800 if not self.repo_name:
800 801 self.repo_name = get_repo_slug(request)
801 802
802 803 try:
803 804 self._user_perms = set(
804 805 [self.user_perms['repositories'][self.repo_name]]
805 806 )
806 807 except KeyError:
807 808 return False
808 809 if self.required_perms.issubset(self._user_perms):
809 810 return True
810 811 return False
811 812
812 813
813 814 class HasRepoPermissionAny(PermsFunction):
814 815 def __call__(self, repo_name=None, check_location=''):
815 816 self.repo_name = repo_name
816 817 return super(HasRepoPermissionAny, self).__call__(check_location)
817 818
818 819 def check_permissions(self):
819 820 if not self.repo_name:
820 821 self.repo_name = get_repo_slug(request)
821 822
822 823 try:
823 824 self._user_perms = set(
824 825 [self.user_perms['repositories'][self.repo_name]]
825 826 )
826 827 except KeyError:
827 828 return False
828 829 if self.required_perms.intersection(self._user_perms):
829 830 return True
830 831 return False
831 832
832 833
833 834 class HasReposGroupPermissionAny(PermsFunction):
834 835 def __call__(self, group_name=None, check_location=''):
835 836 self.group_name = group_name
836 837 return super(HasReposGroupPermissionAny, self).__call__(check_location)
837 838
838 839 def check_permissions(self):
839 840 try:
840 841 self._user_perms = set(
841 842 [self.user_perms['repositories_groups'][self.group_name]]
842 843 )
843 844 except KeyError:
844 845 return False
845 846 if self.required_perms.intersection(self._user_perms):
846 847 return True
847 848 return False
848 849
849 850
850 851 class HasReposGroupPermissionAll(PermsFunction):
851 852 def __call__(self, group_name=None, check_location=''):
852 853 self.group_name = group_name
853 854 return super(HasReposGroupPermissionAll, self).__call__(check_location)
854 855
855 856 def check_permissions(self):
856 857 try:
857 858 self._user_perms = set(
858 859 [self.user_perms['repositories_groups'][self.group_name]]
859 860 )
860 861 except KeyError:
861 862 return False
862 863 if self.required_perms.issubset(self._user_perms):
863 864 return True
864 865 return False
865 866
866 867
867 868 #==============================================================================
868 869 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
869 870 #==============================================================================
870 871 class HasPermissionAnyMiddleware(object):
871 872 def __init__(self, *perms):
872 873 self.required_perms = set(perms)
873 874
874 875 def __call__(self, user, repo_name):
875 876 # repo_name MUST be unicode, since we handle keys in permission
876 877 # dict by unicode
877 878 repo_name = safe_unicode(repo_name)
878 879 usr = AuthUser(user.user_id)
879 880 try:
880 881 self.user_perms = set([usr.permissions['repositories'][repo_name]])
881 882 except Exception:
882 883 log.error('Exception while accessing permissions %s' %
883 884 traceback.format_exc())
884 885 self.user_perms = set()
885 886 self.username = user.username
886 887 self.repo_name = repo_name
887 888 return self.check_permissions()
888 889
889 890 def check_permissions(self):
890 891 log.debug('checking VCS protocol '
891 892 'permissions %s for user:%s repository:%s', self.user_perms,
892 893 self.username, self.repo_name)
893 894 if self.required_perms.intersection(self.user_perms):
894 895 log.debug('permission granted for user:%s on repo:%s' % (
895 896 self.username, self.repo_name
896 897 )
897 898 )
898 899 return True
899 900 log.debug('permission denied for user:%s on repo:%s' % (
900 901 self.username, self.repo_name
901 902 )
902 903 )
903 904 return False
904 905
905 906
906 907 #==============================================================================
907 908 # SPECIAL VERSION TO HANDLE API AUTH
908 909 #==============================================================================
909 910 class _BaseApiPerm(object):
910 911 def __init__(self, *perms):
911 912 self.required_perms = set(perms)
912 913
913 914 def __call__(self, check_location='unspecified', user=None, repo_name=None):
914 915 cls_name = self.__class__.__name__
915 916 check_scope = 'user:%s, repo:%s' % (user, repo_name)
916 917 log.debug('checking cls:%s %s %s @ %s', cls_name,
917 918 self.required_perms, check_scope, check_location)
918 919 if not user:
919 920 log.debug('Empty User passed into arguments')
920 921 return False
921 922
922 923 ## process user
923 924 if not isinstance(user, AuthUser):
924 925 user = AuthUser(user.user_id)
925 926
926 927 if self.check_permissions(user.permissions, repo_name):
927 928 log.debug('Permission to %s granted for user: %s @ %s', repo_name,
928 929 user, check_location)
929 930 return True
930 931
931 932 else:
932 933 log.debug('Permission to %s denied for user: %s @ %s', repo_name,
933 934 user, check_location)
934 935 return False
935 936
936 937 def check_permissions(self, perm_defs, repo_name):
937 938 """
938 939 implement in child class should return True if permissions are ok,
939 940 False otherwise
940 941
941 942 :param perm_defs: dict with permission definitions
942 943 :param repo_name: repo name
943 944 """
944 945 raise NotImplementedError()
945 946
946 947
947 948 class HasPermissionAllApi(_BaseApiPerm):
948 949 def __call__(self, user, check_location=''):
949 950 return super(HasPermissionAllApi, self)\
950 951 .__call__(check_location=check_location, user=user)
951 952
952 953 def check_permissions(self, perm_defs, repo):
953 954 if self.required_perms.issubset(perm_defs.get('global')):
954 955 return True
955 956 return False
956 957
957 958
958 959 class HasPermissionAnyApi(_BaseApiPerm):
959 960 def __call__(self, user, check_location=''):
960 961 return super(HasPermissionAnyApi, self)\
961 962 .__call__(check_location=check_location, user=user)
962 963
963 964 def check_permissions(self, perm_defs, repo):
964 965 if self.required_perms.intersection(perm_defs.get('global')):
965 966 return True
966 967 return False
967 968
968 969
969 970 class HasRepoPermissionAllApi(_BaseApiPerm):
970 971 def __call__(self, user, repo_name, check_location=''):
971 972 return super(HasRepoPermissionAllApi, self)\
972 973 .__call__(check_location=check_location, user=user,
973 974 repo_name=repo_name)
974 975
975 976 def check_permissions(self, perm_defs, repo_name):
976 977
977 978 try:
978 979 self._user_perms = set(
979 980 [perm_defs['repositories'][repo_name]]
980 981 )
981 982 except KeyError:
982 983 log.warning(traceback.format_exc())
983 984 return False
984 985 if self.required_perms.issubset(self._user_perms):
985 986 return True
986 987 return False
987 988
988 989
989 990 class HasRepoPermissionAnyApi(_BaseApiPerm):
990 991 def __call__(self, user, repo_name, check_location=''):
991 992 return super(HasRepoPermissionAnyApi, self)\
992 993 .__call__(check_location=check_location, user=user,
993 994 repo_name=repo_name)
994 995
995 996 def check_permissions(self, perm_defs, repo_name):
996 997
997 998 try:
998 999 _user_perms = set(
999 1000 [perm_defs['repositories'][repo_name]]
1000 1001 )
1001 1002 except KeyError:
1002 1003 log.warning(traceback.format_exc())
1003 1004 return False
1004 1005 if self.required_perms.intersection(_user_perms):
1005 1006 return True
1006 1007 return False
1007 1008
1008 1009
1009 1010 def check_ip_access(source_ip, allowed_ips=None):
1010 1011 """
1011 1012 Checks if source_ip is a subnet of any of allowed_ips.
1012 1013
1013 1014 :param source_ip:
1014 1015 :param allowed_ips: list of allowed ips together with mask
1015 1016 """
1016 1017 from rhodecode.lib import ipaddr
1017 1018 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1018 1019 if isinstance(allowed_ips, (tuple, list, set)):
1019 1020 for ip in allowed_ips:
1020 1021 try:
1021 1022 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1022 1023 return True
1023 1024 # for any case we cannot determine the IP, don't crash just
1024 1025 # skip it and log as error, we want to say forbidden still when
1025 1026 # sending bad IP
1026 1027 except Exception:
1027 1028 log.error(traceback.format_exc())
1028 1029 continue
1029 1030 return False
@@ -1,164 +1,167 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changelog
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode authentication library for LDAP
7 7
8 8 :created_on: Created on Nov 17, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
29 LdapPasswordError
29 LdapPasswordError, LdapImportError
30 30 from rhodecode.lib.utils2 import safe_str
31 31
32 32 log = logging.getLogger(__name__)
33 33
34 34
35 35 try:
36 36 import ldap
37 37 except ImportError:
38 38 # means that python-ldap is not installed
39 pass
39 ldap = None
40 40
41 41
42 42 class AuthLdap(object):
43 43
44 44 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
45 45 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
46 46 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
47 47 search_scope='SUBTREE', attr_login='uid'):
48 if ldap is None:
49 raise LdapImportError
50
48 51 self.ldap_version = ldap_version
49 52 ldap_server_type = 'ldap'
50 53
51 54 self.TLS_KIND = tls_kind
52 55
53 56 if self.TLS_KIND == 'LDAPS':
54 57 port = port or 689
55 58 ldap_server_type = ldap_server_type + 's'
56 59
57 60 OPT_X_TLS_DEMAND = 2
58 61 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
59 62 OPT_X_TLS_DEMAND)
60 63 # split server into list
61 64 self.LDAP_SERVER_ADDRESS = server.split(',')
62 65 self.LDAP_SERVER_PORT = port
63 66
64 67 # USE FOR READ ONLY BIND TO LDAP SERVER
65 68 self.LDAP_BIND_DN = safe_str(bind_dn)
66 69 self.LDAP_BIND_PASS = safe_str(bind_pass)
67 70 _LDAP_SERVERS = []
68 71 for host in self.LDAP_SERVER_ADDRESS:
69 72 _LDAP_SERVERS.append("%s://%s:%s" % (ldap_server_type,
70 73 host.replace(' ', ''),
71 74 self.LDAP_SERVER_PORT))
72 75 self.LDAP_SERVER = str(', '.join(s for s in _LDAP_SERVERS))
73 76 self.BASE_DN = safe_str(base_dn)
74 77 self.LDAP_FILTER = safe_str(ldap_filter)
75 78 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
76 79 self.attr_login = attr_login
77 80
78 81 def authenticate_ldap(self, username, password):
79 82 """
80 83 Authenticate a user via LDAP and return his/her LDAP properties.
81 84
82 85 Raises AuthenticationError if the credentials are rejected, or
83 86 EnvironmentError if the LDAP server can't be reached.
84 87
85 88 :param username: username
86 89 :param password: password
87 90 """
88 91
89 92 from rhodecode.lib.helpers import chop_at
90 93
91 94 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
92 95
93 96 if not password:
94 97 log.debug("Attempt to authenticate LDAP user "
95 98 "with blank password rejected.")
96 99 raise LdapPasswordError()
97 100 if "," in username:
98 101 raise LdapUsernameError("invalid character in username: ,")
99 102 try:
100 103 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
101 104 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
102 105 '/etc/openldap/cacerts')
103 106 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
104 107 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
105 108 ldap.set_option(ldap.OPT_TIMEOUT, 20)
106 109 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
107 110 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
108 111 if self.TLS_KIND != 'PLAIN':
109 112 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
110 113 server = ldap.initialize(self.LDAP_SERVER)
111 114 if self.ldap_version == 2:
112 115 server.protocol = ldap.VERSION2
113 116 else:
114 117 server.protocol = ldap.VERSION3
115 118
116 119 if self.TLS_KIND == 'START_TLS':
117 120 server.start_tls_s()
118 121
119 122 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
120 123 log.debug('Trying simple_bind with password and given DN: %s'
121 124 % self.LDAP_BIND_DN)
122 125 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
123 126
124 127 filter_ = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
125 128 username)
126 129 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
127 130 filter_, self.LDAP_SERVER)
128 131 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE,
129 132 filter_)
130 133
131 134 if not lobjects:
132 135 raise ldap.NO_SUCH_OBJECT()
133 136
134 137 for (dn, _attrs) in lobjects:
135 138 if dn is None:
136 139 continue
137 140
138 141 try:
139 142 log.debug('Trying simple bind with %s' % dn)
140 143 server.simple_bind_s(dn, password)
141 144 attrs = server.search_ext_s(dn, ldap.SCOPE_BASE,
142 145 '(objectClass=*)')[0][1]
143 146 break
144 147
145 148 except ldap.INVALID_CREDENTIALS:
146 149 log.debug(
147 150 "LDAP rejected password for user '%s' (%s): %s" % (
148 151 uid, username, dn
149 152 )
150 153 )
151 154
152 155 else:
153 156 log.debug("No matching LDAP objects for authentication "
154 157 "of '%s' (%s)", uid, username)
155 158 raise LdapPasswordError()
156 159
157 160 except ldap.NO_SUCH_OBJECT:
158 161 log.debug("LDAP says no such user '%s' (%s)" % (uid, username))
159 162 raise LdapUsernameError()
160 163 except ldap.SERVER_DOWN:
161 164 raise LdapConnectionError("LDAP can't access "
162 165 "authentication server")
163 166
164 167 return (dn, attrs)
General Comments 0
You need to be logged in to leave comments. Login now