##// END OF EJS Templates
users: fix crash when creating users with non ASCII characters...
Mads Kiilerich -
r5697:4db2e72c stable
parent child Browse files
Show More
@@ -1,516 +1,516 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.user
16 16 ~~~~~~~~~~~~~~~~~~~~
17 17
18 18 users model for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 9, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import hashlib
30 30 import hmac
31 31 import logging
32 32 import time
33 33 import traceback
34 34
35 35 from pylons import config
36 36 from pylons.i18n.translation import _
37 37
38 38 from sqlalchemy.exc import DatabaseError
39 39
40 40 from kallithea import EXTERN_TYPE_INTERNAL
41 41 from kallithea.lib.utils2 import safe_unicode, generate_api_key, get_current_authuser
42 42 from kallithea.lib.caching_query import FromCache
43 43 from kallithea.model import BaseModel
44 44 from kallithea.model.db import User, UserToPerm, Notification, \
45 45 UserEmailMap, UserIpMap
46 46 from kallithea.lib.exceptions import DefaultUserException, \
47 47 UserOwnsReposException
48 48 from kallithea.model.meta import Session
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 password_reset_token_lifetime = 86400 # 24 hours
56 56
57 57 cls = User
58 58
59 59 def get(self, user_id, cache=False):
60 60 user = self.sa.query(User)
61 61 if cache:
62 62 user = user.options(FromCache("sql_cache_short",
63 63 "get_user_%s" % user_id))
64 64 return user.get(user_id)
65 65
66 66 def get_user(self, user):
67 67 return self._get_user(user)
68 68
69 69 def create(self, form_data, cur_user=None):
70 70 if not cur_user:
71 71 cur_user = getattr(get_current_authuser(), 'username', None)
72 72
73 73 from kallithea.lib.hooks import log_create_user, \
74 74 check_allowed_create_user
75 75 _fd = form_data
76 76 user_data = {
77 77 'username': _fd['username'],
78 78 'password': _fd['password'],
79 79 'email': _fd['email'],
80 80 'firstname': _fd['firstname'],
81 81 'lastname': _fd['lastname'],
82 82 'active': _fd['active'],
83 83 'admin': False
84 84 }
85 85 # raises UserCreationError if it's not allowed
86 86 check_allowed_create_user(user_data, cur_user)
87 87 from kallithea.lib.auth import get_crypt_password
88 88
89 89 new_user = User()
90 90 for k, v in form_data.items():
91 91 if k == 'password':
92 92 v = get_crypt_password(v)
93 93 if k == 'firstname':
94 94 k = 'name'
95 95 setattr(new_user, k, v)
96 96
97 97 new_user.api_key = generate_api_key()
98 98 self.sa.add(new_user)
99 99
100 100 log_create_user(new_user.get_dict(), cur_user)
101 101 return new_user
102 102
103 103 def create_or_update(self, username, password, email, firstname='',
104 104 lastname='', active=True, admin=False,
105 105 extern_type=None, extern_name=None, cur_user=None):
106 106 """
107 107 Creates a new instance if not found, or updates current one
108 108
109 109 :param username:
110 110 :param password:
111 111 :param email:
112 112 :param active:
113 113 :param firstname:
114 114 :param lastname:
115 115 :param active:
116 116 :param admin:
117 117 :param extern_name:
118 118 :param extern_type:
119 119 :param cur_user:
120 120 """
121 121 if not cur_user:
122 122 cur_user = getattr(get_current_authuser(), 'username', None)
123 123
124 124 from kallithea.lib.auth import get_crypt_password, check_password
125 125 from kallithea.lib.hooks import log_create_user, \
126 126 check_allowed_create_user
127 127 user_data = {
128 128 'username': username, 'password': password,
129 129 'email': email, 'firstname': firstname, 'lastname': lastname,
130 130 'active': active, 'admin': admin
131 131 }
132 132 # raises UserCreationError if it's not allowed
133 133 check_allowed_create_user(user_data, cur_user)
134 134
135 135 log.debug('Checking for %s account in Kallithea database', username)
136 136 user = User.get_by_username(username, case_insensitive=True)
137 137 if user is None:
138 138 log.debug('creating new user %s', username)
139 139 new_user = User()
140 140 edit = False
141 141 else:
142 142 log.debug('updating user %s', username)
143 143 new_user = user
144 144 edit = True
145 145
146 146 try:
147 147 new_user.username = username
148 148 new_user.admin = admin
149 149 new_user.email = email
150 150 new_user.active = active
151 151 new_user.extern_name = safe_unicode(extern_name) \
152 152 if extern_name else None
153 153 new_user.extern_type = safe_unicode(extern_type) \
154 154 if extern_type else None
155 155 new_user.name = firstname
156 156 new_user.lastname = lastname
157 157
158 158 if not edit:
159 159 new_user.api_key = generate_api_key()
160 160
161 161 # set password only if creating an user or password is changed
162 162 password_change = new_user.password and \
163 163 not check_password(password, new_user.password)
164 164 if not edit or password_change:
165 165 reason = 'new password' if edit else 'new user'
166 166 log.debug('Updating password reason=>%s', reason)
167 167 new_user.password = get_crypt_password(password) \
168 168 if password else None
169 169
170 170 self.sa.add(new_user)
171 171
172 172 if not edit:
173 173 log_create_user(new_user.get_dict(), cur_user)
174 174 return new_user
175 175 except (DatabaseError,):
176 176 log.error(traceback.format_exc())
177 177 raise
178 178
179 179 def create_registration(self, form_data):
180 180 from kallithea.model.notification import NotificationModel
181 181 import kallithea.lib.helpers as h
182 182
183 183 form_data['admin'] = False
184 184 form_data['extern_name'] = EXTERN_TYPE_INTERNAL
185 185 form_data['extern_type'] = EXTERN_TYPE_INTERNAL
186 186 new_user = self.create(form_data)
187 187
188 188 self.sa.add(new_user)
189 189 self.sa.flush()
190 190
191 191 # notification to admins
192 192 subject = _('New user registration')
193 193 body = (
194 'New user registration\n'
194 u'New user registration\n'
195 195 '---------------------\n'
196 196 '- Username: {user.username}\n'
197 197 '- Full Name: {user.full_name}\n'
198 198 '- Email: {user.email}\n'
199 199 ).format(user=new_user)
200 200 edit_url = h.canonical_url('edit_user', id=new_user.user_id)
201 201 email_kwargs = {
202 202 'registered_user_url': edit_url,
203 203 'new_username': new_user.username}
204 204 NotificationModel().create(created_by=new_user, subject=subject,
205 205 body=body, recipients=None,
206 206 type_=Notification.TYPE_REGISTRATION,
207 207 email_kwargs=email_kwargs)
208 208
209 209 def update(self, user_id, form_data, skip_attrs=[]):
210 210 from kallithea.lib.auth import get_crypt_password
211 211
212 212 user = self.get(user_id, cache=False)
213 213 if user.username == User.DEFAULT_USER:
214 214 raise DefaultUserException(
215 215 _("You can't edit this user since it's "
216 216 "crucial for entire application"))
217 217
218 218 for k, v in form_data.items():
219 219 if k in skip_attrs:
220 220 continue
221 221 if k == 'new_password' and v:
222 222 user.password = get_crypt_password(v)
223 223 else:
224 224 # old legacy thing orm models store firstname as name,
225 225 # need proper refactor to username
226 226 if k == 'firstname':
227 227 k = 'name'
228 228 setattr(user, k, v)
229 229 self.sa.add(user)
230 230
231 231 def update_user(self, user, **kwargs):
232 232 from kallithea.lib.auth import get_crypt_password
233 233
234 234 user = self._get_user(user)
235 235 if user.username == User.DEFAULT_USER:
236 236 raise DefaultUserException(
237 237 _("You can't edit this user since it's"
238 238 " crucial for entire application")
239 239 )
240 240
241 241 for k, v in kwargs.items():
242 242 if k == 'password' and v:
243 243 v = get_crypt_password(v)
244 244
245 245 setattr(user, k, v)
246 246 self.sa.add(user)
247 247 return user
248 248
249 249 def delete(self, user, cur_user=None):
250 250 if cur_user is None:
251 251 cur_user = getattr(get_current_authuser(), 'username', None)
252 252 user = self._get_user(user)
253 253
254 254 if user.username == User.DEFAULT_USER:
255 255 raise DefaultUserException(
256 256 _("You can't remove this user since it is"
257 257 " crucial for the entire application"))
258 258 if user.repositories:
259 259 repos = [x.repo_name for x in user.repositories]
260 260 raise UserOwnsReposException(
261 261 _('User "%s" still owns %s repositories and cannot be '
262 262 'removed. Switch owners or remove those repositories: %s')
263 263 % (user.username, len(repos), ', '.join(repos)))
264 264 if user.repo_groups:
265 265 repogroups = [x.group_name for x in user.repo_groups]
266 266 raise UserOwnsReposException(_(
267 267 'User "%s" still owns %s repository groups and cannot be '
268 268 'removed. Switch owners or remove those repository groups: %s')
269 269 % (user.username, len(repogroups), ', '.join(repogroups)))
270 270 if user.user_groups:
271 271 usergroups = [x.users_group_name for x in user.user_groups]
272 272 raise UserOwnsReposException(
273 273 _('User "%s" still owns %s user groups and cannot be '
274 274 'removed. Switch owners or remove those user groups: %s')
275 275 % (user.username, len(usergroups), ', '.join(usergroups)))
276 276 self.sa.delete(user)
277 277
278 278 from kallithea.lib.hooks import log_delete_user
279 279 log_delete_user(user.get_dict(), cur_user)
280 280
281 281 def get_reset_password_token(self, user, timestamp, session_id):
282 282 """
283 283 The token is a 40-digit hexstring, calculated as a HMAC-SHA1.
284 284
285 285 In a traditional HMAC scenario, an attacker is unable to know or
286 286 influence the secret key, but can know or influence the message
287 287 and token. This scenario is slightly different (in particular
288 288 since the message sender is also the message recipient), but
289 289 sufficiently similar to use an HMAC. Benefits compared to a plain
290 290 SHA1 hash includes resistance against a length extension attack.
291 291
292 292 The HMAC key consists of the following values (known only to the
293 293 server and authorized users):
294 294
295 295 * per-application secret (the `app_instance_uuid` setting), without
296 296 which an attacker cannot counterfeit tokens
297 297 * hashed user password, invalidating the token upon password change
298 298
299 299 The HMAC message consists of the following values (potentially known
300 300 to an attacker):
301 301
302 302 * session ID (the anti-CSRF token), requiring an attacker to have
303 303 access to the browser session in which the token was created
304 304 * numeric user ID, limiting the token to a specific user (yet allowing
305 305 users to be renamed)
306 306 * user email address
307 307 * time of token issue (a Unix timestamp, to enable token expiration)
308 308
309 309 The key and message values are separated by NUL characters, which are
310 310 guaranteed not to occur in any of the values.
311 311 """
312 312 app_secret = config.get('app_instance_uuid')
313 313 return hmac.HMAC(
314 314 key=u'\0'.join([app_secret, user.password]).encode('utf-8'),
315 315 msg=u'\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
316 316 digestmod=hashlib.sha1,
317 317 ).hexdigest()
318 318
319 319 def send_reset_password_email(self, data):
320 320 """
321 321 Sends email with a password reset token and link to the password
322 322 reset confirmation page with all information (including the token)
323 323 pre-filled. Also returns URL of that page, only without the token,
324 324 allowing users to copy-paste or manually enter the token from the
325 325 email.
326 326 """
327 327 from kallithea.lib.celerylib import tasks, run_task
328 328 from kallithea.model.notification import EmailNotificationModel
329 329 import kallithea.lib.helpers as h
330 330
331 331 user_email = data['email']
332 332 user = User.get_by_email(user_email)
333 333 timestamp = int(time.time())
334 334 if user is not None:
335 335 log.debug('password reset user %s found', user)
336 336 token = self.get_reset_password_token(user,
337 337 timestamp,
338 338 h.authentication_token())
339 339 # URL must be fully qualified; but since the token is locked to
340 340 # the current browser session, we must provide a URL with the
341 341 # current scheme and hostname, rather than the canonical_url.
342 342 link = h.url('reset_password_confirmation', qualified=True,
343 343 email=user_email,
344 344 timestamp=timestamp,
345 345 token=token)
346 346
347 347 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
348 348 body = EmailNotificationModel().get_email_tmpl(
349 349 reg_type, 'txt',
350 350 user=user.short_contact,
351 351 reset_token=token,
352 352 reset_url=link)
353 353 html_body = EmailNotificationModel().get_email_tmpl(
354 354 reg_type, 'html',
355 355 user=user.short_contact,
356 356 reset_token=token,
357 357 reset_url=link)
358 358 log.debug('sending email')
359 359 run_task(tasks.send_email, [user_email],
360 360 _("Password reset link"), body, html_body)
361 361 log.info('send new password mail to %s', user_email)
362 362 else:
363 363 log.debug("password reset email %s not found", user_email)
364 364
365 365 return h.url('reset_password_confirmation',
366 366 email=user_email,
367 367 timestamp=timestamp)
368 368
369 369 def verify_reset_password_token(self, email, timestamp, token):
370 370 from kallithea.lib.celerylib import tasks, run_task
371 371 from kallithea.lib import auth
372 372 import kallithea.lib.helpers as h
373 373 user = User.get_by_email(email)
374 374 if user is None:
375 375 log.debug("user with email %s not found", email)
376 376 return False
377 377
378 378 token_age = int(time.time()) - int(timestamp)
379 379
380 380 if token_age < 0:
381 381 log.debug('timestamp is from the future')
382 382 return False
383 383
384 384 if token_age > UserModel.password_reset_token_lifetime:
385 385 log.debug('password reset token expired')
386 386 return False
387 387
388 388 expected_token = self.get_reset_password_token(user,
389 389 timestamp,
390 390 h.authentication_token())
391 391 log.debug('computed password reset token: %s', expected_token)
392 392 log.debug('received password reset token: %s', token)
393 393 return expected_token == token
394 394
395 395 def reset_password(self, user_email, new_passwd):
396 396 from kallithea.lib.celerylib import tasks, run_task
397 397 from kallithea.lib import auth
398 398 user = User.get_by_email(user_email)
399 399 if user is not None:
400 400 user.password = auth.get_crypt_password(new_passwd)
401 401 Session().add(user)
402 402 Session().commit()
403 403 log.info('change password for %s', user_email)
404 404 if new_passwd is None:
405 405 raise Exception('unable to set new password')
406 406
407 407 run_task(tasks.send_email, [user_email],
408 408 _('Password reset notification'),
409 409 _('The password to your account %s has been changed using password reset form.') % (user.username,))
410 410 log.info('send password reset mail to %s', user_email)
411 411
412 412 return True
413 413
414 414 def has_perm(self, user, perm):
415 415 perm = self._get_perm(perm)
416 416 user = self._get_user(user)
417 417
418 418 return UserToPerm.query().filter(UserToPerm.user == user)\
419 419 .filter(UserToPerm.permission == perm).scalar() is not None
420 420
421 421 def grant_perm(self, user, perm):
422 422 """
423 423 Grant user global permissions
424 424
425 425 :param user:
426 426 :param perm:
427 427 """
428 428 user = self._get_user(user)
429 429 perm = self._get_perm(perm)
430 430 # if this permission is already granted skip it
431 431 _perm = UserToPerm.query()\
432 432 .filter(UserToPerm.user == user)\
433 433 .filter(UserToPerm.permission == perm)\
434 434 .scalar()
435 435 if _perm:
436 436 return
437 437 new = UserToPerm()
438 438 new.user = user
439 439 new.permission = perm
440 440 self.sa.add(new)
441 441 return new
442 442
443 443 def revoke_perm(self, user, perm):
444 444 """
445 445 Revoke users global permissions
446 446
447 447 :param user:
448 448 :param perm:
449 449 """
450 450 user = self._get_user(user)
451 451 perm = self._get_perm(perm)
452 452
453 453 UserToPerm.query().filter(
454 454 UserToPerm.user == user,
455 455 UserToPerm.permission == perm,
456 456 ).delete()
457 457
458 458 def add_extra_email(self, user, email):
459 459 """
460 460 Adds email address to UserEmailMap
461 461
462 462 :param user:
463 463 :param email:
464 464 """
465 465 from kallithea.model import forms
466 466 form = forms.UserExtraEmailForm()()
467 467 data = form.to_python(dict(email=email))
468 468 user = self._get_user(user)
469 469
470 470 obj = UserEmailMap()
471 471 obj.user = user
472 472 obj.email = data['email']
473 473 self.sa.add(obj)
474 474 return obj
475 475
476 476 def delete_extra_email(self, user, email_id):
477 477 """
478 478 Removes email address from UserEmailMap
479 479
480 480 :param user:
481 481 :param email_id:
482 482 """
483 483 user = self._get_user(user)
484 484 obj = UserEmailMap.query().get(email_id)
485 485 if obj is not None:
486 486 self.sa.delete(obj)
487 487
488 488 def add_extra_ip(self, user, ip):
489 489 """
490 490 Adds IP address to UserIpMap
491 491
492 492 :param user:
493 493 :param ip:
494 494 """
495 495 from kallithea.model import forms
496 496 form = forms.UserExtraIpForm()()
497 497 data = form.to_python(dict(ip=ip))
498 498 user = self._get_user(user)
499 499
500 500 obj = UserIpMap()
501 501 obj.user = user
502 502 obj.ip_addr = data['ip']
503 503 self.sa.add(obj)
504 504 return obj
505 505
506 506 def delete_extra_ip(self, user, ip_id):
507 507 """
508 508 Removes IP address from UserIpMap
509 509
510 510 :param user:
511 511 :param ip_id:
512 512 """
513 513 user = self._get_user(user)
514 514 obj = UserIpMap.query().get(ip_id)
515 515 if obj:
516 516 self.sa.delete(obj)
General Comments 0
You need to be logged in to leave comments. Login now