##// END OF EJS Templates
fixed issues with not unique emails when using ldap or container auth.
marcink -
r1690:6944b124 beta
parent child Browse files
Show More
@@ -1,154 +1,153 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) 2009-2011 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 29 LdapPasswordError
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 try:
35 35 import ldap
36 36 except ImportError:
37 37 # means that python-ldap is not installed
38 38 pass
39 39
40 40
41 41 class AuthLdap(object):
42 42
43 43 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
44 44 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
45 45 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
46 search_scope='SUBTREE',
47 attr_login='uid'):
46 search_scope = 'SUBTREE', attr_login = 'uid'):
48 47 self.ldap_version = ldap_version
49 48 ldap_server_type = 'ldap'
50 49
51 50 self.TLS_KIND = tls_kind
52 51
53 52 if self.TLS_KIND == 'LDAPS':
54 53 port = port or 689
55 54 ldap_server_type = ldap_server_type + 's'
56 55
57 56 OPT_X_TLS_DEMAND = 2
58 57 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
59 58 OPT_X_TLS_DEMAND)
60 59 self.LDAP_SERVER_ADDRESS = server
61 60 self.LDAP_SERVER_PORT = port
62 61
63 62 #USE FOR READ ONLY BIND TO LDAP SERVER
64 63 self.LDAP_BIND_DN = bind_dn
65 64 self.LDAP_BIND_PASS = bind_pass
66 65
67 66 self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
68 67 self.LDAP_SERVER_ADDRESS,
69 68 self.LDAP_SERVER_PORT)
70 69
71 70 self.BASE_DN = base_dn
72 71 self.LDAP_FILTER = ldap_filter
73 72 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
74 73 self.attr_login = attr_login
75 74
76 75 def authenticate_ldap(self, username, password):
77 76 """Authenticate a user via LDAP and return his/her LDAP properties.
78 77
79 78 Raises AuthenticationError if the credentials are rejected, or
80 79 EnvironmentError if the LDAP server can't be reached.
81 80
82 81 :param username: username
83 82 :param password: password
84 83 """
85 84
86 85 from rhodecode.lib.helpers import chop_at
87 86
88 87 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
89 88
90 89 if not password:
91 90 log.debug("Attempt to authenticate LDAP user with blank password rejected.")
92 91 raise LdapPasswordError()
93 92 if "," in username:
94 93 raise LdapUsernameError("invalid character in username: ,")
95 94 try:
96 95 if hasattr(ldap,'OPT_X_TLS_CACERTDIR'):
97 96 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
98 97 '/etc/openldap/cacerts')
99 98 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
100 99 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
101 100 ldap.set_option(ldap.OPT_TIMEOUT, 20)
102 101 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
103 102 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
104 103 if self.TLS_KIND != 'PLAIN':
105 104 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
106 105 server = ldap.initialize(self.LDAP_SERVER)
107 106 if self.ldap_version == 2:
108 107 server.protocol = ldap.VERSION2
109 108 else:
110 109 server.protocol = ldap.VERSION3
111 110
112 111 if self.TLS_KIND == 'START_TLS':
113 112 server.start_tls_s()
114 113
115 114 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
116 115 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
117 116
118 117 filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
119 118 username)
120 119 log.debug("Authenticating %r filt %s at %s", self.BASE_DN,
121 120 filt, self.LDAP_SERVER)
122 121 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE,
123 122 filt)
124 123
125 124 if not lobjects:
126 125 raise ldap.NO_SUCH_OBJECT()
127 126
128 127 for (dn, _attrs) in lobjects:
129 128 if dn is None:
130 129 continue
131 130
132 131 try:
133 132 server.simple_bind_s(dn, password)
134 133 attrs = server.search_ext_s(dn, ldap.SCOPE_BASE,
135 134 '(objectClass=*)')[0][1]
136 135 break
137 136
138 137 except ldap.INVALID_CREDENTIALS, e:
139 138 log.debug("LDAP rejected password for user '%s' (%s): %s",
140 139 uid, username, dn)
141 140
142 141 else:
143 142 log.debug("No matching LDAP objects for authentication "
144 143 "of '%s' (%s)", uid, username)
145 144 raise LdapPasswordError()
146 145
147 146 except ldap.NO_SUCH_OBJECT, e:
148 147 log.debug("LDAP says no such user '%s' (%s)", uid, username)
149 148 raise LdapUsernameError()
150 149 except ldap.SERVER_DOWN, e:
151 150 raise LdapConnectionError("LDAP can't access "
152 151 "authentication server")
153 152
154 153 return (dn, attrs)
@@ -1,476 +1,480 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.user
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 users model for RhodeCode
7 7
8 8 :created_on: Apr 9, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2009-2011 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 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30
31 31 from rhodecode.lib import safe_unicode
32 32 from rhodecode.lib.caching_query import FromCache
33 33
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
36 36 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember
37 37 from rhodecode.lib.exceptions import DefaultUserException, \
38 38 UserOwnsReposException
39 39
40 40 from sqlalchemy.exc import DatabaseError
41 41 from rhodecode.lib import generate_api_key
42 42 from sqlalchemy.orm import joinedload
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46 PERM_WEIGHTS = {'repository.none': 0,
47 47 'repository.read': 1,
48 48 'repository.write': 3,
49 49 'repository.admin': 3}
50 50
51 51
52 52 class UserModel(BaseModel):
53 53 def get(self, user_id, cache=False):
54 54 user = self.sa.query(User)
55 55 if cache:
56 56 user = user.options(FromCache("sql_cache_short",
57 57 "get_user_%s" % user_id))
58 58 return user.get(user_id)
59 59
60 60 def get_by_username(self, username, cache=False, case_insensitive=False):
61 61
62 62 if case_insensitive:
63 63 user = self.sa.query(User).filter(User.username.ilike(username))
64 64 else:
65 65 user = self.sa.query(User)\
66 66 .filter(User.username == username)
67 67 if cache:
68 68 user = user.options(FromCache("sql_cache_short",
69 69 "get_user_%s" % username))
70 70 return user.scalar()
71 71
72 72 def get_by_api_key(self, api_key, cache=False):
73 73
74 74 user = self.sa.query(User)\
75 75 .filter(User.api_key == api_key)
76 76 if cache:
77 77 user = user.options(FromCache("sql_cache_short",
78 78 "get_user_%s" % api_key))
79 79 return user.scalar()
80 80
81 81 def create(self, form_data):
82 82 try:
83 83 new_user = User()
84 84 for k, v in form_data.items():
85 85 setattr(new_user, k, v)
86 86
87 87 new_user.api_key = generate_api_key(form_data['username'])
88 88 self.sa.add(new_user)
89 89 self.sa.commit()
90 90 return new_user
91 91 except:
92 92 log.error(traceback.format_exc())
93 93 self.sa.rollback()
94 94 raise
95 95
96 96
97 97 def create_or_update(self, username, password, email, name, lastname,
98 98 active=True, admin=False, ldap_dn=None):
99 99 """
100 100 Creates a new instance if not found, or updates current one
101 101
102 102 :param username:
103 103 :param password:
104 104 :param email:
105 105 :param active:
106 106 :param name:
107 107 :param lastname:
108 108 :param active:
109 109 :param admin:
110 110 :param ldap_dn:
111 111 """
112 112
113 113 from rhodecode.lib.auth import get_crypt_password
114 114
115 115 log.debug('Checking for %s account in RhodeCode database', username)
116 116 user = User.get_by_username(username, case_insensitive=True)
117 117 if user is None:
118 118 log.debug('creating new user %s', username)
119 119 new_user = User()
120 120 else:
121 121 log.debug('updating user %s', username)
122 122 new_user = user
123 123
124 124 try:
125 125 new_user.username = username
126 126 new_user.admin = admin
127 127 new_user.password = get_crypt_password(password)
128 128 new_user.api_key = generate_api_key(username)
129 129 new_user.email = email
130 130 new_user.active = active
131 131 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
132 132 new_user.name = name
133 133 new_user.lastname = lastname
134 134
135 135 self.sa.add(new_user)
136 136 self.sa.commit()
137 137 return new_user
138 138 except (DatabaseError,):
139 139 log.error(traceback.format_exc())
140 140 self.sa.rollback()
141 141 raise
142 142
143 143
144 144 def create_for_container_auth(self, username, attrs):
145 145 """
146 146 Creates the given user if it's not already in the database
147 147
148 148 :param username:
149 149 :param attrs:
150 150 """
151 151 if self.get_by_username(username, case_insensitive=True) is None:
152
153 # autogenerate email for container account without one
154 generate_email = lambda usr: '%s@container_auth.account' % usr
155
152 156 try:
153 157 new_user = User()
154 158 new_user.username = username
155 159 new_user.password = None
156 160 new_user.api_key = generate_api_key(username)
157 161 new_user.email = attrs['email']
158 162 new_user.active = attrs.get('active', True)
159 new_user.name = attrs['name']
163 new_user.name = attrs['name'] or generate_email(username)
160 164 new_user.lastname = attrs['lastname']
161 165
162 166 self.sa.add(new_user)
163 167 self.sa.commit()
164 168 return new_user
165 169 except (DatabaseError,):
166 170 log.error(traceback.format_exc())
167 171 self.sa.rollback()
168 172 raise
169 173 log.debug('User %s already exists. Skipping creation of account'
170 174 ' for container auth.', username)
171 175 return None
172 176
173 177 def create_ldap(self, username, password, user_dn, attrs):
174 178 """
175 179 Checks if user is in database, if not creates this user marked
176 180 as ldap user
177 181
178 182 :param username:
179 183 :param password:
180 184 :param user_dn:
181 185 :param attrs:
182 186 """
183 187 from rhodecode.lib.auth import get_crypt_password
184 188 log.debug('Checking for such ldap account in RhodeCode database')
185 189 if self.get_by_username(username, case_insensitive=True) is None:
186 190
187 191 # autogenerate email for ldap account without one
188 192 generate_email = lambda usr: '%s@ldap.account' % usr
189 193
190 194 try:
191 195 new_user = User()
192 196 username = username.lower()
193 197 # add ldap account always lowercase
194 198 new_user.username = username
195 199 new_user.password = get_crypt_password(password)
196 200 new_user.api_key = generate_api_key(username)
197 201 new_user.email = attrs['email'] or generate_email(username)
198 202 new_user.active = attrs.get('active', True)
199 203 new_user.ldap_dn = safe_unicode(user_dn)
200 204 new_user.name = attrs['name']
201 205 new_user.lastname = attrs['lastname']
202 206
203 207 self.sa.add(new_user)
204 208 self.sa.commit()
205 209 return new_user
206 210 except (DatabaseError,):
207 211 log.error(traceback.format_exc())
208 212 self.sa.rollback()
209 213 raise
210 214 log.debug('this %s user exists skipping creation of ldap account',
211 215 username)
212 216 return None
213 217
214 218 def create_registration(self, form_data):
215 219 from rhodecode.lib.celerylib import tasks, run_task
216 220 try:
217 221 new_user = User()
218 222 for k, v in form_data.items():
219 223 if k != 'admin':
220 224 setattr(new_user, k, v)
221 225
222 226 self.sa.add(new_user)
223 227 self.sa.commit()
224 228 body = ('New user registration\n'
225 229 'username: %s\n'
226 230 'email: %s\n')
227 231 body = body % (form_data['username'], form_data['email'])
228 232
229 233 run_task(tasks.send_email, None,
230 234 _('[RhodeCode] New User registration'),
231 235 body)
232 236 except:
233 237 log.error(traceback.format_exc())
234 238 self.sa.rollback()
235 239 raise
236 240
237 241 def update(self, user_id, form_data):
238 242 try:
239 243 user = self.get(user_id, cache=False)
240 244 if user.username == 'default':
241 245 raise DefaultUserException(
242 246 _("You can't Edit this user since it's"
243 247 " crucial for entire application"))
244 248
245 249 for k, v in form_data.items():
246 250 if k == 'new_password' and v != '':
247 251 user.password = v
248 252 user.api_key = generate_api_key(user.username)
249 253 else:
250 254 setattr(user, k, v)
251 255
252 256 self.sa.add(user)
253 257 self.sa.commit()
254 258 except:
255 259 log.error(traceback.format_exc())
256 260 self.sa.rollback()
257 261 raise
258 262
259 263 def update_my_account(self, user_id, form_data):
260 264 try:
261 265 user = self.get(user_id, cache=False)
262 266 if user.username == 'default':
263 267 raise DefaultUserException(
264 268 _("You can't Edit this user since it's"
265 269 " crucial for entire application"))
266 270 for k, v in form_data.items():
267 271 if k == 'new_password' and v != '':
268 272 user.password = v
269 273 user.api_key = generate_api_key(user.username)
270 274 else:
271 275 if k not in ['admin', 'active']:
272 276 setattr(user, k, v)
273 277
274 278 self.sa.add(user)
275 279 self.sa.commit()
276 280 except:
277 281 log.error(traceback.format_exc())
278 282 self.sa.rollback()
279 283 raise
280 284
281 285 def delete(self, user_id):
282 286 try:
283 287 user = self.get(user_id, cache=False)
284 288 if user.username == 'default':
285 289 raise DefaultUserException(
286 290 _("You can't remove this user since it's"
287 291 " crucial for entire application"))
288 292 if user.repositories:
289 293 raise UserOwnsReposException(_('This user still owns %s '
290 294 'repositories and cannot be '
291 295 'removed. Switch owners or '
292 296 'remove those repositories') \
293 297 % user.repositories)
294 298 self.sa.delete(user)
295 299 self.sa.commit()
296 300 except:
297 301 log.error(traceback.format_exc())
298 302 self.sa.rollback()
299 303 raise
300 304
301 305 def reset_password_link(self, data):
302 306 from rhodecode.lib.celerylib import tasks, run_task
303 307 run_task(tasks.send_password_link, data['email'])
304 308
305 309 def reset_password(self, data):
306 310 from rhodecode.lib.celerylib import tasks, run_task
307 311 run_task(tasks.reset_user_password, data['email'])
308 312
309 313 def fill_data(self, auth_user, user_id=None, api_key=None):
310 314 """
311 315 Fetches auth_user by user_id,or api_key if present.
312 316 Fills auth_user attributes with those taken from database.
313 317 Additionally set's is_authenitated if lookup fails
314 318 present in database
315 319
316 320 :param auth_user: instance of user to set attributes
317 321 :param user_id: user id to fetch by
318 322 :param api_key: api key to fetch by
319 323 """
320 324 if user_id is None and api_key is None:
321 325 raise Exception('You need to pass user_id or api_key')
322 326
323 327 try:
324 328 if api_key:
325 329 dbuser = self.get_by_api_key(api_key)
326 330 else:
327 331 dbuser = self.get(user_id)
328 332
329 333 if dbuser is not None and dbuser.active:
330 334 log.debug('filling %s data', dbuser)
331 335 for k, v in dbuser.get_dict().items():
332 336 setattr(auth_user, k, v)
333 337 else:
334 338 return False
335 339
336 340 except:
337 341 log.error(traceback.format_exc())
338 342 auth_user.is_authenticated = False
339 343 return False
340 344
341 345 return True
342 346
343 347 def fill_perms(self, user):
344 348 """
345 349 Fills user permission attribute with permissions taken from database
346 350 works for permissions given for repositories, and for permissions that
347 351 are granted to groups
348 352
349 353 :param user: user instance to fill his perms
350 354 """
351 355
352 356 user.permissions['repositories'] = {}
353 357 user.permissions['global'] = set()
354 358
355 359 #======================================================================
356 360 # fetch default permissions
357 361 #======================================================================
358 362 default_user = self.get_by_username('default', cache=True)
359 363
360 364 default_perms = self.sa.query(UserRepoToPerm, Repository, Permission)\
361 365 .join((Repository, UserRepoToPerm.repository_id ==
362 366 Repository.repo_id))\
363 367 .join((Permission, UserRepoToPerm.permission_id ==
364 368 Permission.permission_id))\
365 369 .filter(UserRepoToPerm.user == default_user).all()
366 370
367 371 if user.is_admin:
368 372 #==================================================================
369 373 # #admin have all default rights set to admin
370 374 #==================================================================
371 375 user.permissions['global'].add('hg.admin')
372 376
373 377 for perm in default_perms:
374 378 p = 'repository.admin'
375 379 user.permissions['repositories'][perm.UserRepoToPerm.
376 380 repository.repo_name] = p
377 381
378 382 else:
379 383 #==================================================================
380 384 # set default permissions
381 385 #==================================================================
382 386 uid = user.user_id
383 387
384 388 #default global
385 389 default_global_perms = self.sa.query(UserToPerm)\
386 390 .filter(UserToPerm.user == default_user)
387 391
388 392 for perm in default_global_perms:
389 393 user.permissions['global'].add(perm.permission.permission_name)
390 394
391 395 #default for repositories
392 396 for perm in default_perms:
393 397 if perm.Repository.private and not (perm.Repository.user_id ==
394 398 uid):
395 399 #diself.sable defaults for private repos,
396 400 p = 'repository.none'
397 401 elif perm.Repository.user_id == uid:
398 402 #set admin if owner
399 403 p = 'repository.admin'
400 404 else:
401 405 p = perm.Permission.permission_name
402 406
403 407 user.permissions['repositories'][perm.UserRepoToPerm.
404 408 repository.repo_name] = p
405 409
406 410 #==================================================================
407 411 # overwrite default with user permissions if any
408 412 #==================================================================
409 413
410 414 #user global
411 415 user_perms = self.sa.query(UserToPerm)\
412 416 .options(joinedload(UserToPerm.permission))\
413 417 .filter(UserToPerm.user_id == uid).all()
414 418
415 419 for perm in user_perms:
416 420 user.permissions['global'].add(perm.permission.
417 421 permission_name)
418 422
419 423 #user repositories
420 424 user_repo_perms = self.sa.query(UserRepoToPerm, Permission,
421 425 Repository)\
422 426 .join((Repository, UserRepoToPerm.repository_id ==
423 427 Repository.repo_id))\
424 428 .join((Permission, UserRepoToPerm.permission_id ==
425 429 Permission.permission_id))\
426 430 .filter(UserRepoToPerm.user_id == uid).all()
427 431
428 432 for perm in user_repo_perms:
429 433 # set admin if owner
430 434 if perm.Repository.user_id == uid:
431 435 p = 'repository.admin'
432 436 else:
433 437 p = perm.Permission.permission_name
434 438 user.permissions['repositories'][perm.UserRepoToPerm.
435 439 repository.repo_name] = p
436 440
437 441 #==================================================================
438 442 # check if user is part of groups for this repository and fill in
439 443 # (or replace with higher) permissions
440 444 #==================================================================
441 445
442 446 #users group global
443 447 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
444 448 .options(joinedload(UsersGroupToPerm.permission))\
445 449 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
446 450 UsersGroupMember.users_group_id))\
447 451 .filter(UsersGroupMember.user_id == uid).all()
448 452
449 453 for perm in user_perms_from_users_groups:
450 454 user.permissions['global'].add(perm.permission.permission_name)
451 455
452 456 #users group repositories
453 457 user_repo_perms_from_users_groups = self.sa.query(
454 458 UsersGroupRepoToPerm,
455 459 Permission, Repository,)\
456 460 .join((Repository, UsersGroupRepoToPerm.repository_id ==
457 461 Repository.repo_id))\
458 462 .join((Permission, UsersGroupRepoToPerm.permission_id ==
459 463 Permission.permission_id))\
460 464 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id ==
461 465 UsersGroupMember.users_group_id))\
462 466 .filter(UsersGroupMember.user_id == uid).all()
463 467
464 468 for perm in user_repo_perms_from_users_groups:
465 469 p = perm.Permission.permission_name
466 470 cur_perm = user.permissions['repositories'][perm.
467 471 UsersGroupRepoToPerm.
468 472 repository.repo_name]
469 473 #overwrite permission only if it's greater than permission
470 474 # given from other sources
471 475 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
472 476 user.permissions['repositories'][perm.UsersGroupRepoToPerm.
473 477 repository.repo_name] = p
474 478
475 479 return user
476 480
General Comments 0
You need to be logged in to leave comments. Login now