]> git.ipfire.org Git - people/jschlag/pbs.git/blob - backend/users.py
97a132676c114a6fb111bcdc8a990fab45daad96
[people/jschlag/pbs.git] / backend / users.py
1 #!/usr/bin/python
2
3 import hashlib
4 import logging
5 import pytz
6 import random
7 import re
8 import string
9 import urllib
10
11 import tornado.locale
12
13 import base
14
15 # A list of possible random characters.
16 random_chars = string.ascii_letters + string.digits
17
18 def generate_random_string(length=16):
19 """
20 Return a string with random chararcters A-Za-z0-9 with given length.
21 """
22 return "".join([random.choice(random_chars) for i in range(length)])
23
24
25 def generate_password_hash(password, salt=None, algo="sha512"):
26 """
27 This function creates a salted digest of the given password.
28 """
29 # Generate the salt (length = 16) of none was given.
30 if salt is None:
31 salt = generate_random_string(length=16)
32
33 # Compute the hash.
34 # <SALT> + <PASSWORD>
35 if not algo in hashlib.algorithms:
36 raise Exception, "Unsupported password hash algorithm: %s" % algo
37
38 # Calculate the digest.
39 h = hashlib.new(algo)
40 h.update(salt)
41 h.update(password)
42
43 # Output string is of kind "<algo>$<salt>$<hash>".
44 return "$".join((algo, salt, h.hexdigest()))
45
46 def check_password_hash(password, password_hash):
47 """
48 Check a plain-text password with the given digest.
49 """
50 # Handle plaintext passwords (plain$<password>).
51 if password_hash.startswith("plain$"):
52 return password_hash[6:] == password
53
54 try:
55 algo, salt, digest = password_hash.split("$", 2)
56 except ValueError:
57 logging.warning("Unknown password hash: %s" % password_hash)
58 return False
59
60 # Re-generate the password hash and compare the result.
61 return password_hash == generate_password_hash(password, salt=salt, algo=algo)
62
63 def check_password_strength(password):
64 score = 0
65 accepted = False
66
67 # Empty passwords cannot be used.
68 if len(password) == 0:
69 return False, 0
70
71 # Passwords with less than 6 characters are also too weak.
72 if len(password) < 6:
73 return False, 1
74
75 # Password with at least 8 characters are secure.
76 if len(password) >= 8:
77 score += 1
78
79 # 10 characters are even more secure.
80 if len(password) >= 10:
81 score += 1
82
83 # Digits in the password are good.
84 if re.search("\d+", password):
85 score += 1
86
87 # Check for lowercase AND uppercase characters.
88 if re.search("[a-z]", password) and re.search("[A-Z]", password):
89 score += 1
90
91 # Search for special characters.
92 if re.search(".[!,@,#,$,%,^,&,*,?,_,~,-,(,)]", password):
93 score += 1
94
95 if score >= 3:
96 accepted = True
97
98 return accepted, score
99
100 def maintainer_split(s):
101 m = re.match(r"(.*) <(.*)>", s)
102 if m:
103 name, email = m.groups()
104 else:
105 name, email = None, None
106
107 return name, email
108
109 class Users(base.Object):
110 def auth(self, name, password):
111 # If either name or password is None, we don't check at all.
112 if None in (name, password):
113 return
114
115 # Search for the username in the database.
116 # The user must not be deleted and must be activated.
117 user = self.db.get("SELECT id FROM users WHERE name = %s AND \
118 activated = 'Y' AND deleted = 'N'", name)
119
120 if not user:
121 return
122
123 # Get the whole User object from the database.
124 user = self.get_by_id(user.id)
125
126 # If the user was not found or the password does not match,
127 # you aren't lucky.
128 if not user or not user.check_password(password):
129 return
130
131 # Otherwise we return the User object.
132 return user
133
134 def register(self, name, password, email, realname, locale=None):
135 return User.new(self.pakfire, name, password, email, realname, locale)
136
137 def name_is_used(self, name):
138 users = self.db.query("SELECT id FROM users WHERE name = %s", name)
139
140 if users:
141 return True
142
143 return False
144
145 def email_is_used(self, email):
146 users = self.db.query("SELECT id FROM users_emails WHERE email = %s", email)
147
148 if users:
149 return True
150
151 return False
152
153 def get_all(self):
154 users = self.db.query("""SELECT id FROM users WHERE activated = 'Y' AND
155 deleted = 'N' ORDER BY name ASC""")
156
157 return [User(self.pakfire, u.id) for u in users]
158
159 def get_by_id(self, id):
160 return User(self.pakfire, id)
161
162 def get_by_name(self, name):
163 user = self.db.get("SELECT id FROM users WHERE name = %s LIMIT 1", name)
164
165 if user:
166 return User(self.pakfire, user.id)
167
168 def get_by_email(self, email):
169 user = self.db.get("SELECT user_id AS id FROM users_emails \
170 WHERE email = %s LIMIT 1", email)
171
172 if user:
173 return User(self.pakfire, user.id)
174
175 def count(self):
176 users = self.db.get("SELECT COUNT(*) AS count FROM users \
177 WHERE activated = 'Y' AND deleted = 'N'")
178
179 if users:
180 return users.count
181
182 def search(self, pattern, limit=None):
183 pattern = "%%%s%%" % pattern
184
185 query = "SELECT id FROM users \
186 WHERE (name LIKE %s OR realname LIKE %s) AND activated = %s AND deleted = %s"
187 args = [pattern, pattern, "Y", "N"]
188
189 if limit:
190 query += " LIMIT %s"
191 args.append(limit)
192
193 users = []
194 for user in self.db.query(query, *args):
195 user = User(self.pakfire, user.id)
196 users.append(user)
197
198 return users
199
200 def find_maintainer(self, s):
201 if not s:
202 return
203
204 name, email = maintainer_split(s)
205 if not email:
206 return
207
208 user = self.db.get("SELECT user_id FROM users_emails WHERE email = %s LIMIT 1", email)
209 if not user:
210 return
211
212 return self.get_by_id(user.user_id)
213
214
215 class User(base.Object):
216 def __init__(self, pakfire, id):
217 base.Object.__init__(self, pakfire)
218 self.id = id
219
220 # A valid session of the user.
221 self.session = None
222
223 # Cache.
224 self._data = None
225 self._emails = None
226 self._perms = None
227
228 def __repr__(self):
229 return "<%s %s>" % (self.__class__.__name__, self.realname)
230
231 def __hash__(self):
232 return hash(self.id)
233
234 def __cmp__(self, other):
235 if other is None:
236 return 1
237
238 if isinstance(other, unicode):
239 return cmp(self.email, other)
240
241 if self.id == other.id:
242 return 0
243
244 return cmp(self.realname, other.realname)
245
246 @classmethod
247 def new(cls, pakfire, name, passphrase, email, realname, locale=None):
248 id = pakfire.db.execute("INSERT INTO users(name, passphrase, realname) \
249 VALUES(%s, %s, %s)", name, generate_password_hash(passphrase), realname)
250
251 # Add email address.
252 pakfire.db.execute("INSERT INTO users_emails(user_id, email, `primary`) \
253 VALUES(%s, %s, 'Y')", id, email)
254
255 # Create row in permissions table.
256 pakfire.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", id)
257
258 user = cls(pakfire, id)
259
260 # If we have a guessed locale, we save it (for sending emails).
261 if locale:
262 user.locale = locale
263
264 user.send_activation_mail()
265
266 return user
267
268 @property
269 def data(self):
270 if self._data is None:
271 self._data = self.db.get("SELECT * FROM users WHERE id = %s" % self.id)
272 assert self._data, "User %s not found." % self.id
273
274 return self._data
275
276 def delete(self):
277 self.db.execute("UPDATE users SET deleted = 'Y' WHERE id = %s", self.id)
278 self._data = None
279
280 def activate(self):
281 self.db.execute("UPDATE users SET activated = 'Y', activation_code = NULL \
282 WHERE id = %s", self.id)
283
284 def check_password(self, password):
285 """
286 Compare the given password with the one stored in the database.
287 """
288 return check_password_hash(password, self.data.passphrase)
289
290 def set_passphrase(self, passphrase):
291 """
292 Update the passphrase the users uses to log on.
293 """
294 self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s",
295 generate_password_hash(passphrase), self.id)
296
297 passphrase = property(lambda x: None, set_passphrase)
298
299 @property
300 def activation_code(self):
301 return self.data.activation_code
302
303 def get_realname(self):
304 if not self.data.realname:
305 return self.name
306
307 return self.data.realname
308
309 def set_realname(self, realname):
310 self.db.execute("UPDATE users SET realname = %s WHERE id = %s",
311 realname, self.id)
312 self.data["realname"] = realname
313
314 realname = property(get_realname, set_realname)
315
316 @property
317 def name(self):
318 return self.data.name
319
320 @property
321 def firstname(self):
322 # Try to split the string into first and last name.
323 # If that is not successful, return the entire realname.
324 try:
325 firstname, rest = self.realname.split(" ", 1)
326 except:
327 return self.realname
328
329 return firstname
330
331 def get_email(self):
332 if self._emails is None:
333 self._emails = self.db.query("SELECT * FROM users_emails WHERE user_id = %s", self.id)
334 assert self._emails
335
336 for email in self._emails:
337 if not email.primary == "Y":
338 continue
339
340 return email.email
341
342 def set_email(self, email):
343 if email == self.email:
344 return
345
346 self.db.execute("UPDATE users_emails SET email = %s \
347 WHERE user_id = %s AND primary = 'Y'", email, self.id)
348
349 self.db.execute("UPDATE users SET activated 'N' WHERE id = %s",
350 email, self.id)
351
352 # Reset cache.
353 self._data = self._emails = None
354
355 # Inform the user, that he or she has to re-activate the account.
356 self.send_activation_mail()
357
358 email = property(get_email, set_email)
359
360 def get_state(self):
361 return self.data.state
362
363 def set_state(self, state):
364 self.db.execute("UPDATE users SET state = %s WHERE id = %s", state,
365 self.id)
366 self.data["state"] = state
367
368 state = property(get_state, set_state)
369
370 def get_locale(self):
371 return self.data.locale or ""
372
373 def set_locale(self, locale):
374 self.db.execute("UPDATE users SET locale = %s WHERE id = %s", locale,
375 self.id)
376 self.data["locale"] = locale
377
378 locale = property(get_locale, set_locale)
379
380 def get_timezone(self, tz=None):
381 if tz is None:
382 tz = self.data.timezone or ""
383
384 try:
385 tz = pytz.timezone(tz)
386 except pytz.UnknownTimeZoneError:
387 tz = pytz.timezone("UTC")
388
389 return tz
390
391 def set_timezone(self, timezone):
392 if not timezone is None:
393 tz = self.get_timezone(timezone)
394 timezone = tz.zone
395
396 self.db.execute("UPDATE users SET timezone = %s WHERE id = %s",
397 timezone, self.id)
398
399 timezone = property(get_timezone, set_timezone)
400
401 @property
402 def activated(self):
403 return self.data.activated == "Y"
404
405 @property
406 def registered(self):
407 return self.data.registered
408
409 def gravatar_icon(self, size=128):
410 # construct the url
411 gravatar_url = "http://www.gravatar.com/avatar/" + \
412 hashlib.md5(self.email.lower()).hexdigest() + "?"
413 gravatar_url += urllib.urlencode({'d': "mm", 's': str(size)})
414
415 return gravatar_url
416
417 def is_admin(self):
418 return self.state == "admin"
419
420 def is_tester(self):
421 return self.state == "tester"
422
423 @property
424 def perms(self):
425 if self._perms is None:
426 self._perms = \
427 self.db.get("SELECT * FROM users_permissions WHERE user_id = %s", self.id)
428
429 return self._perms
430
431 def has_perm(self, perm):
432 """
433 Returns True if the user has the requested permission.
434 """
435 # Admins have the permission for everything.
436 if self.is_admin():
437 return True
438
439 # Exception for voting. All testers are allowed to vote.
440 if perm == "vote" and self.is_tester():
441 return True
442
443 # All others must be checked individually.
444 return self.perms.get(perm, "N") == "Y"
445
446 def send_activation_mail(self):
447 logging.debug("Sending activation mail to %s" % self.email)
448
449 # Generate a random activation code.
450 source = string.ascii_letters + string.digits
451 self.data["activation_code"] = "".join(random.sample(source * 20, 20))
452 self.db.execute("UPDATE users SET activation_code = %s WHERE id = %s",
453 self.activation_code, self.id)
454
455 # Get the saved locale from the user.
456 locale = tornado.locale.get(self.locale)
457 _ = locale.translate
458
459 subject = _("Account Activation")
460
461 message = _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.")
462 message += "\n"*2
463 message += _("To activate your account, please click on the link below.")
464 message += "\n"*2
465 message += " %(baseurl)s/user/%(name)s/activate?code=%(activation_code)s" \
466 % { "baseurl" : self.settings.get("baseurl"), "name" : self.name,
467 "activation_code" : self.activation_code, }
468 message += "\n"*2
469 message += "Sincerely,\n The Pakfire Build Service"
470
471 self.pakfire.messages.add("%s <%s>" % (self.realname, self.email), subject, message)
472
473
474 # Some testing code.
475 if __name__ == "__main__":
476 for password in ("1234567890", "abcdefghij"):
477 digest = generate_password_hash(password)
478
479 print "%s %s" % (password, digest)
480 print " Matches? %s" % check_password_hash(password, digest)
481