]> git.ipfire.org Git - people/jschlag/pbs.git/blame - src/buildservice/users.py
Move the check_password function into the users class
[people/jschlag/pbs.git] / src / buildservice / users.py
CommitLineData
9137135a
MT
1#!/usr/bin/python
2
3import hashlib
4import logging
f6e6ff79 5import pytz
9137135a 6import random
f6e6ff79 7import re
9137135a
MT
8import string
9import urllib
10
11import tornado.locale
12
2c909128 13from . import base
9137135a 14
f6e6ff79
MT
15# A list of possible random characters.
16random_chars = string.ascii_letters + string.digits
17
18def 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
25def 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
46def 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
f6e6ff79
MT
63def maintainer_split(s):
64 m = re.match(r"(.*) <(.*)>", s)
65 if m:
66 name, email = m.groups()
67 else:
68 name, email = None, None
69
70 return name, email
71
9137135a 72class Users(base.Object):
f6e6ff79
MT
73 def auth(self, name, password):
74 # If either name or password is None, we don't check at all.
75 if None in (name, password):
9137135a
MT
76 return
77
f6e6ff79
MT
78 # Search for the username in the database.
79 # The user must not be deleted and must be activated.
80 user = self.db.get("SELECT id FROM users WHERE name = %s AND \
81 activated = 'Y' AND deleted = 'N'", name)
9137135a 82
f6e6ff79
MT
83 if not user:
84 return
9137135a 85
f6e6ff79
MT
86 # Get the whole User object from the database.
87 user = self.get_by_id(user.id)
88
89 # If the user was not found or the password does not match,
90 # you aren't lucky.
91 if not user or not user.check_password(password):
92 return
93
94 # Otherwise we return the User object.
95 return user
96
97 def register(self, name, password, email, realname, locale=None):
4ac87d61
JS
98 user = User.new(self.pakfire, name, email, realname, locale)
99 user.passphrase = password
100 return user
9137135a
MT
101
102 def name_is_used(self, name):
103 users = self.db.query("SELECT id FROM users WHERE name = %s", name)
104
105 if users:
106 return True
107
108 return False
109
110 def email_is_used(self, email):
f6e6ff79 111 users = self.db.query("SELECT id FROM users_emails WHERE email = %s", email)
9137135a
MT
112
113 if users:
114 return True
115
116 return False
117
118 def get_all(self):
119 users = self.db.query("""SELECT id FROM users WHERE activated = 'Y' AND
3da206e1 120 deleted = 'N' ORDER BY name ASC""")
9137135a
MT
121
122 return [User(self.pakfire, u.id) for u in users]
123
124 def get_by_id(self, id):
f6e6ff79 125 return User(self.pakfire, id)
9137135a
MT
126
127 def get_by_name(self, name):
128 user = self.db.get("SELECT id FROM users WHERE name = %s LIMIT 1", name)
129
130 if user:
131 return User(self.pakfire, user.id)
132
133 def get_by_email(self, email):
f6e6ff79
MT
134 user = self.db.get("SELECT user_id AS id FROM users_emails \
135 WHERE email = %s LIMIT 1", email)
9137135a
MT
136
137 if user:
138 return User(self.pakfire, user.id)
139
f6e6ff79 140 def count(self):
966498de
MT
141 users = self.db.get("SELECT COUNT(*) AS count FROM users \
142 WHERE activated = 'Y' AND deleted = 'N'")
f6e6ff79 143
966498de
MT
144 if users:
145 return users.count
f6e6ff79
MT
146
147 def search(self, pattern, limit=None):
c9619eec
MT
148 pattern = "%%%s%%" % pattern
149
f6e6ff79 150 query = "SELECT id FROM users \
c9619eec
MT
151 WHERE (name LIKE %s OR realname LIKE %s) AND activated = %s AND deleted = %s"
152 args = [pattern, pattern, "Y", "N"]
f6e6ff79
MT
153
154 if limit:
155 query += " LIMIT %s"
156 args.append(limit)
157
158 users = []
159 for user in self.db.query(query, *args):
160 user = User(self.pakfire, user.id)
161 users.append(user)
162
163 return users
164
165 def find_maintainer(self, s):
166 if not s:
167 return
168
169 name, email = maintainer_split(s)
170 if not email:
171 return
172
173 user = self.db.get("SELECT user_id FROM users_emails WHERE email = %s LIMIT 1", email)
174 if not user:
175 return
176
177 return self.get_by_id(user.user_id)
c0b302b3
JS
178
179 @staticmethod
180 def check_password_strength(password):
181 score = 0
182 accepted = False
183
184 # Empty passwords cannot be used.
185 if len(password) == 0:
186 return False, 0
187
188 # Passwords with less than 6 characters are also too weak.
189 if len(password) < 6:
190 return False, 1
191
192 # Password with at least 8 characters are secure.
193 if len(password) >= 8:
194 score += 1
195
196 # 10 characters are even more secure.
197 if len(password) >= 10:
198 score += 1
199
200 # Digits in the password are good.
201 if re.search("\d+", password):
202 score += 1
203
204 # Check for lowercase AND uppercase characters.
205 if re.search("[a-z]", password) and re.search("[A-Z]", password):
206 score += 1
207
208 # Search for special characters.
209 if re.search(".[!,@,#,$,%,^,&,*,?,_,~,-,(,)]", password):
210 score += 1
211
212 if score >= 3:
213 accepted = True
214
215 return accepted, score
f6e6ff79 216
9137135a
MT
217
218class User(base.Object):
219 def __init__(self, pakfire, id):
220 base.Object.__init__(self, pakfire)
221 self.id = id
222
f6e6ff79
MT
223 # Cache.
224 self._data = None
225 self._emails = None
226 self._perms = None
9137135a 227
20d7f5eb
MT
228 def __repr__(self):
229 return "<%s %s>" % (self.__class__.__name__, self.realname)
230
231 def __hash__(self):
232 return hash(self.id)
233
9137135a 234 def __cmp__(self, other):
f6e6ff79
MT
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)
9137135a
MT
245
246 @classmethod
4ac87d61
JS
247 def new(cls, pakfire, name , email, realname, locale=None):
248 id = pakfire.db.execute("INSERT INTO users(name, realname) \
249 VALUES(%s, %s)", name, realname)
f6e6ff79
MT
250
251 # Add email address.
cb84bbd7 252 pakfire.db.execute("INSERT INTO users_emails(user_id, email, primary) \
f6e6ff79 253 VALUES(%s, %s, 'Y')", id, email)
9137135a 254
f6e6ff79
MT
255 # Create row in permissions table.
256 pakfire.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", id)
9137135a
MT
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
f6e6ff79
MT
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
9137135a
MT
276 def delete(self):
277 self.db.execute("UPDATE users SET deleted = 'Y' WHERE id = %s", self.id)
f6e6ff79 278 self._data = None
9137135a
MT
279
280 def activate(self):
f6e6ff79
MT
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)
9137135a
MT
289
290 def set_passphrase(self, passphrase):
291 """
292 Update the passphrase the users uses to log on.
293 """
f6e6ff79
MT
294 self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s",
295 generate_password_hash(passphrase), self.id)
9137135a
MT
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
f6e6ff79
MT
320 @property
321 def firstname(self):
3f82e940
MT
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
f6e6ff79
MT
328
329 return firstname
330
9137135a 331 def get_email(self):
f6e6ff79
MT
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
9137135a
MT
341
342 def set_email(self, email):
343 if email == self.email:
344 return
345
f6e6ff79
MT
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)
9137135a 351
f6e6ff79
MT
352 # Reset cache.
353 self._data = self._emails = None
9137135a
MT
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
f6e6ff79
MT
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
9137135a
MT
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
f6e6ff79
MT
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
9137135a
MT
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
c6009001 461 message = _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.")
9137135a
MT
462 message += "\n"*2
463 message += _("To activate your account, please click on the link below.")
464 message += "\n"*2
f6e6ff79
MT
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, }
9137135a
MT
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
f6e6ff79
MT
473
474# Some testing code.
475if __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)
9137135a 481