]> git.ipfire.org Git - pbs.git/blob - src/buildservice/users.py
users: Let locale attribute return the tornado locale
[pbs.git] / src / buildservice / users.py
1 #!/usr/bin/python
2
3 import email.utils
4 import hashlib
5 import logging
6 import pytz
7 import random
8 import re
9 import string
10 import urllib
11 import ldap
12
13 import tornado.locale
14
15 log = logging.getLogger("users")
16 log.propagate = 1
17
18 from . import base
19 from . import ldap
20
21 from .decorators import *
22
23 # A list of possible random characters.
24 random_chars = string.ascii_letters + string.digits
25
26 def generate_random_string(length=16):
27 """
28 Return a string with random chararcters A-Za-z0-9 with given length.
29 """
30 return "".join([random.choice(random_chars) for i in range(length)])
31
32
33 def generate_password_hash(password, salt=None, algo="sha512"):
34 """
35 This function creates a salted digest of the given password.
36 """
37 # Generate the salt (length = 16) of none was given.
38 if salt is None:
39 salt = generate_random_string(length=16)
40
41 # Compute the hash.
42 # <SALT> + <PASSWORD>
43 if not algo in hashlib.algorithms:
44 raise Exception, "Unsupported password hash algorithm: %s" % algo
45
46 # Calculate the digest.
47 h = hashlib.new(algo)
48 h.update(salt)
49 h.update(password)
50
51 # Output string is of kind "<algo>$<salt>$<hash>".
52 return "$".join((algo, salt, h.hexdigest()))
53
54 def check_password_hash(password, password_hash):
55 """
56 Check a plain-text password with the given digest.
57 """
58 # Handle plaintext passwords (plain$<password>).
59 if password_hash.startswith("plain$"):
60 return password_hash[6:] == password
61
62 try:
63 algo, salt, digest = password_hash.split("$", 2)
64 except ValueError:
65 logging.warning("Unknown password hash: %s" % password_hash)
66 return False
67
68 # Re-generate the password hash and compare the result.
69 return password_hash == generate_password_hash(password, salt=salt, algo=algo)
70
71
72 class Users(base.Object):
73 def init(self):
74 self.ldap = ldap.LDAP(self.backend)
75
76 def _get_user(self, query, *args):
77 res = self.db.get(query, *args)
78
79 if res:
80 return User(self.backend, res.id, data=res)
81
82 def _get_users(self, query, *args):
83 res = self.db.query(query, *args)
84
85 for row in res:
86 yield User(self.backend, row.id, data=row)
87
88 def _get_user_email(self, query, *args):
89 res = self.db.get(query, *args)
90
91 if res:
92 return UserEmail(self.backend, res.id, data=res)
93
94 def _get_user_emails(self, query, *args):
95 res = self.db.query(query, *args)
96
97 for row in res:
98 yield UserEmail(self.backend, row.id, data=row)
99
100 def __iter__(self):
101 users = self._get_users("SELECT * FROM users \
102 WHERE activated IS TRUE AND deleted IS FALSE ORDER BY name")
103
104 return iter(users)
105
106 def __len__(self):
107 res = self.db.get("SELECT COUNT(*) AS count FROM users \
108 WHERE activated IS TRUE AND deleted IS FALSE")
109
110 return res.count
111
112 def create(self, name, realname=None, ldap_dn=None):
113 # XXX check if username has the correct name
114
115 # Check if name is already taken
116 user = self.get_by_name(name)
117 if user:
118 raise ValueError("Username %s already taken" % name)
119
120 # Create new user
121 user = self._get_user("INSERT INTO users(name, realname, ldap_dn) \
122 VALUES(%s, %s, %s) RETURNING *", name, realname, ldap_dn)
123
124 # Create row in permissions table.
125 self.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", user.id)
126
127 log.debug("Created user %s" % user.name)
128
129 return user
130
131 def create_from_ldap(self, name):
132 log.debug("Creating user %s from LDAP" % name)
133
134 # Get required attributes from LDAP
135 dn, attr = self.ldap.get_user(name, attrlist=["uid", "cn", "mail"])
136 assert dn
137
138 # Create regular user
139 user = self.create(name, realname=attr["cn"][0], ldap_dn=dn)
140 user.activate()
141
142 # Add all email addresses and activate them
143 for email in attr["mail"]:
144 user.add_email(email, activated=True)
145
146 return user
147
148 def auth(self, name, password):
149 # If either name or password is None, we don't check at all.
150 if None in (name, password):
151 return
152
153 # usually we will get an email address as name
154 user = self.get_by_email(name) or self.get_by_name(name)
155
156 if not user:
157 # If no user could be found, we search for a matching user in
158 # the LDAP database
159 if not self.ldap.auth(name, password):
160 return
161
162 # If a LDAP user is found (and password matches), we will
163 # create a new local user with the information from LDAP.
164 user = self.create_from_ldap(name)
165
166 if not user.activated or user.deleted:
167 return
168
169 # Check if the password matches
170 if user.check_password(password):
171 return user
172
173 def email_in_use(self, email):
174 return self._get_user_email("SELECT * FROM users_emails \
175 WHERE email = %s AND activated IS TRUE", email)
176
177 def get_by_id(self, id):
178 return self._get_user("SELECT * FROM users WHERE id = %s", id)
179
180 def get_by_name(self, name):
181 return self._get_user("SELECT * FROM users WHERE name = %s", name)
182
183 def get_by_email(self, email):
184 return self._get_user("SELECT users.* FROM users \
185 LEFT JOIN users_emails ON users.id = users_emails.user_id \
186 WHERE users_emails.email = %s", email)
187
188 def find_maintainers(self, maintainers):
189 email_addresses = []
190
191 # Make a unique list of all email addresses
192 for maintainer in maintainers:
193 name, email_address = email.utils.parseaddr(maintainer)
194
195 if not email_address in email_addresses:
196 email_addresses.append(email_address)
197
198 users = self._get_users("SELECT DISTINCT users.* FROM users \
199 LEFT JOIN users_emails ON users.id = users_emails.user_id \
200 WHERE users_emails.activated IS TRUE \
201 AND users_emails.email = ANY(%s)", email_addresses)
202
203 return sorted(users)
204
205 def find_maintainer(self, s):
206 name, email_address = email.utils.parseaddr(s)
207
208 # Got invalid input
209 if not email_address:
210 return
211
212 return self.get_by_email(email_address)
213
214 def search(self, pattern, limit=None):
215 pattern = "%%%s%%" % pattern
216
217 return self._get_users("SELECT * FROM users \
218 WHERE (name LIKE %s OR realname LIKE %s) \
219 AND activated IS TRUE AND deleted IS FALSE \
220 ORDER BY name LIMIT %s", pattern, pattern, limit)
221
222 @staticmethod
223 def check_password_strength(password):
224 score = 0
225 accepted = False
226
227 # Empty passwords cannot be used.
228 if len(password) == 0:
229 return False, 0
230
231 # Passwords with less than 6 characters are also too weak.
232 if len(password) < 6:
233 return False, 1
234
235 # Password with at least 8 characters are secure.
236 if len(password) >= 8:
237 score += 1
238
239 # 10 characters are even more secure.
240 if len(password) >= 10:
241 score += 1
242
243 # Digits in the password are good.
244 if re.search("\d+", password):
245 score += 1
246
247 # Check for lowercase AND uppercase characters.
248 if re.search("[a-z]", password) and re.search("[A-Z]", password):
249 score += 1
250
251 # Search for special characters.
252 if re.search(".[!,@,#,$,%,^,&,*,?,_,~,-,(,)]", password):
253 score += 1
254
255 if score >= 3:
256 accepted = True
257
258 return accepted, score
259
260
261 class User(base.DataObject):
262 table = "users"
263
264 def __repr__(self):
265 return "<%s %s>" % (self.__class__.__name__, self.realname)
266
267 def __hash__(self):
268 return hash(self.id)
269
270 def __eq__(self, other):
271 if isinstance(other, self.__class__):
272 return self.id == other.id
273
274 def __lt__(self, other):
275 if isinstance(other, self.__class__):
276 return self.name < other.name
277
278 elif isinstance(other, str):
279 return self.name < other
280
281 def delete(self):
282 self._set_attribute("deleted", True)
283
284 def activate(self):
285 self._set_attribute("activated", True)
286
287 def check_password(self, password):
288 """
289 Compare the given password with the one stored in the database.
290 """
291 if self.ldap_dn:
292 return self.backend.users.ldap.bind(self.ldap_dn, password)
293
294 return check_password_hash(password, self.data.passphrase)
295
296 def set_passphrase(self, passphrase):
297 """
298 Update the passphrase the users uses to log on.
299 """
300 self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s",
301 generate_password_hash(passphrase), self.id)
302
303 passphrase = property(lambda x: None, set_passphrase)
304
305 def get_realname(self):
306 return self.data.realname or self.name
307
308 def set_realname(self, realname):
309 self._set_attribute("realname", realname)
310
311 realname = property(get_realname, set_realname)
312
313 @property
314 def name(self):
315 return self.data.name
316
317 @property
318 def ldap_dn(self):
319 return self.data.ldap_dn
320
321 @property
322 def firstname(self):
323 # Try to split the string into first and last name.
324 # If that is not successful, return the entire realname.
325 try:
326 firstname, rest = self.realname.split(" ", 1)
327 except:
328 return self.realname
329
330 return firstname
331
332 @lazy_property
333 def emails(self):
334 res = self.backend.users._get_user_emails("SELECT * FROM users_emails \
335 WHERE user_id = %s AND activated IS TRUE ORDER BY email", self.id)
336
337 return list(res)
338
339 @property
340 def email(self):
341 for email in self.emails:
342 if email.primary:
343 return email
344
345 def get_email(self, email):
346 for e in self.emails:
347 if e == email:
348 return e
349
350 def set_primary_email(self, email):
351 if not email in self.emails:
352 raise ValueError("Email address does not belong to user")
353
354 # Mark previous primary email as non-primary
355 self.db.execute("UPDATE users_emails SET \"primary\" = FALSE \
356 WHERE user_id = %s AND \"primary\" IS TRUE" % self.id)
357
358 # Mark new primary email
359 self.db.execute("UPDATE users_emails SET \"primary\" = TRUE \
360 WHERE user_id = %s AND email = %s AND activated IS TRUE",
361 self.id, email)
362
363 def has_email_address(self, email_address):
364 try:
365 mail, email_address = email.utils.parseaddr(email_address)
366 except:
367 pass
368
369 return email_address in self.emails
370
371 def activate_email(self, code):
372 # Search email by activation code
373 email = self.backend.users._get_user_email("SELECT * FROM users_emails \
374 WHERE user_id = %s AND activated IS FALSE AND activation_code = %s", self.id, code)
375
376 if not email:
377 return False
378
379 # Activate email address
380 email.activate()
381 return True
382
383 # Te activated flag is useful for LDAP users
384 def add_email(self, email, activated=False):
385 # Check if the email is in use
386 if self.backend.users.email_in_use(email):
387 raise ValueError("Email %s is already in use" % email)
388
389 activation_code = None
390 if not activated:
391 activation_code = generate_random_string(64)
392
393 user_email = self.backend.users._get_user_email("INSERT INTO users_emails(user_id, email, \
394 \"primary\", activated, activation_code) VALUES(%s, %s, %s, %s, %s) RETURNING *",
395 self.id, email, not self.emails, activated, activation_code)
396 self.emails.append(user_email)
397
398 # Send activation email if activation is needed
399 if not activated:
400 user_email.send_activation_mail()
401
402 return user_email
403
404 def set_state(self, state):
405 self._set_attribute("state", state)
406
407 state = property(lambda s: s.data.state, set_state)
408
409 def is_admin(self):
410 return self.state == "admin"
411
412 def is_tester(self):
413 return self.state == "tester"
414
415 def get_locale(self):
416 return tornado.locale.get(self.data.locale)
417
418 def set_locale(self, locale):
419 self._set_attribute("locale", locale)
420
421 locale = property(get_locale, set_locale)
422
423 def get_timezone(self, tz=None):
424 if tz is None:
425 tz = self.data.timezone or ""
426
427 try:
428 tz = pytz.timezone(tz)
429 except pytz.UnknownTimeZoneError:
430 tz = pytz.timezone("UTC")
431
432 return tz
433
434 def set_timezone(self, timezone):
435 if not timezone is None:
436 tz = self.get_timezone(timezone)
437 timezone = tz.zone
438
439 self._set_attribute("timezone", timezone)
440
441 timezone = property(get_timezone, set_timezone)
442
443 @property
444 def activated(self):
445 return self.data.activated
446
447 @property
448 def deleted(self):
449 return self.data.deleted
450
451 @property
452 def registered(self):
453 return self.data.registered
454
455 def gravatar_icon(self, size=128):
456 h = hashlib.new("md5")
457 if self.email:
458 h.update("%s" % self.email)
459
460 # construct the url
461 gravatar_url = "http://www.gravatar.com/avatar/%s?" % h.hexdigest()
462 gravatar_url += urllib.urlencode({'d': "mm", 's': str(size)})
463
464 return gravatar_url
465
466 @lazy_property
467 def perms(self):
468 return self.db.get("SELECT * FROM users_permissions WHERE user_id = %s", self.id)
469
470 def has_perm(self, perm):
471 """
472 Returns True if the user has the requested permission.
473 """
474 # Admins have the permission for everything.
475 if self.is_admin():
476 return True
477
478 # Exception for voting. All testers are allowed to vote.
479 if perm == "vote" and self.is_tester():
480 return True
481
482 # All others must be checked individually.
483 return self.perms.get(perm, False) == True
484
485 @property
486 def sessions(self):
487 return self.backend.sessions._get_sessions("SELECT * FROM sessions \
488 WHERE user_id = %s AND valid_until >= NOW() ORDER BY created_at")
489
490
491 class UserEmail(base.DataObject):
492 table = "users_emails"
493
494 def __str__(self):
495 return self.email
496
497 def __eq__(self, other):
498 if isinstance(other, self.__class__):
499 return self.id == other.id
500
501 elif isinstance(other, str):
502 return self.email == other
503
504 @lazy_property
505 def user(self):
506 return self.backend.users.get_by_id(self.data.user_id)
507
508 @property
509 def recipient(self):
510 return "%s <%s>" % (self.user.name, self.email)
511
512 @property
513 def email(self):
514 return self.data.email
515
516 def set_primary(self, primary):
517 self._set_attribute("primary", primary)
518
519 primary = property(lambda s: s.data.primary, set_primary)
520
521 @property
522 def activated(self):
523 return self.data.activated
524
525 def activate(self):
526 self._set_attribute("activated", True)
527 self._set_attribute("activation_code", None)
528
529 @property
530 def activation_code(self):
531 return self.data.activation_code
532
533 def send_activation_mail(self):
534 if not self.primary:
535 return self.send_email_activation_email()
536
537 logging.debug("Sending activation mail to %s" % self.email)
538
539 # Get the saved locale from the user.
540 _ = tornado.locale.get(self.user.locale).translate
541
542 subject = _("Account Activation")
543
544 message = _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.")
545 message += "\n"*2
546 message += _("To activate your account, please click on the link below.")
547 message += "\n"*2
548 message += " %(baseurl)s/user/%(name)s/activate?code=%(activation_code)s" \
549 % { "baseurl" : self.settings.get("baseurl"), "name" : self.user.name,
550 "activation_code" : self.activation_code, }
551 message += "\n"*2
552 message += "Sincerely,\n The Pakfire Build Service"
553
554 self.backend.messages.add(self.recipient, subject, message)
555
556 def send_email_activation_mail(self, email):
557 logging.debug("Sending email address activation mail to %s" % self.email)
558
559 # Get the saved locale from the user.
560 _ = self.user.locale.translate
561
562 subject = _("Email address Activation")
563
564 message = _("You, or somebody using your email address, has add this email address to an account on the Pakfire Build Service.")
565 message += "\n"*2
566 message += _("To activate your this email address account, please click on the link below.")
567 message += "\n"*2
568 message += " %(baseurl)s/user/%(name)s/activate?code=%(activation_code)s" \
569 % { "baseurl" : self.settings.get("baseurl"), "name" : self.user.name,
570 "activation_code" : self.activation_code, }
571 message += "\n"*2
572 message += "Sincerely,\n The Pakfire Build Service"
573
574 self.backend.messages.add(self.recipient, subject, message)
575
576
577 # Some testing code.
578 if __name__ == "__main__":
579 for password in ("1234567890", "abcdefghij"):
580 digest = generate_password_hash(password)
581
582 print "%s %s" % (password, digest)
583 print " Matches? %s" % check_password_hash(password, digest)
584