]> git.ipfire.org Git - pbs.git/blob - src/buildservice/users.py
98ccf51337924edeed62fbe9887b9e0a3f2b5501
[pbs.git] / src / buildservice / 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 from . 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 # Cache.
221 self._data = None
222 self._emails = None
223 self._perms = None
224
225 def __repr__(self):
226 return "<%s %s>" % (self.__class__.__name__, self.realname)
227
228 def __hash__(self):
229 return hash(self.id)
230
231 def __cmp__(self, other):
232 if other is None:
233 return 1
234
235 if isinstance(other, unicode):
236 return cmp(self.email, other)
237
238 if self.id == other.id:
239 return 0
240
241 return cmp(self.realname, other.realname)
242
243 @classmethod
244 def new(cls, pakfire, name, passphrase, email, realname, locale=None):
245 id = pakfire.db.execute("INSERT INTO users(name, passphrase, realname) \
246 VALUES(%s, %s, %s)", name, generate_password_hash(passphrase), realname)
247
248 # Add email address.
249 pakfire.db.execute("INSERT INTO users_emails(user_id, email, `primary`) \
250 VALUES(%s, %s, 'Y')", id, email)
251
252 # Create row in permissions table.
253 pakfire.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", id)
254
255 user = cls(pakfire, id)
256
257 # If we have a guessed locale, we save it (for sending emails).
258 if locale:
259 user.locale = locale
260
261 user.send_activation_mail()
262
263 return user
264
265 @property
266 def data(self):
267 if self._data is None:
268 self._data = self.db.get("SELECT * FROM users WHERE id = %s" % self.id)
269 assert self._data, "User %s not found." % self.id
270
271 return self._data
272
273 def delete(self):
274 self.db.execute("UPDATE users SET deleted = 'Y' WHERE id = %s", self.id)
275 self._data = None
276
277 def activate(self):
278 self.db.execute("UPDATE users SET activated = 'Y', activation_code = NULL \
279 WHERE id = %s", self.id)
280
281 def check_password(self, password):
282 """
283 Compare the given password with the one stored in the database.
284 """
285 return check_password_hash(password, self.data.passphrase)
286
287 def set_passphrase(self, passphrase):
288 """
289 Update the passphrase the users uses to log on.
290 """
291 self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s",
292 generate_password_hash(passphrase), self.id)
293
294 passphrase = property(lambda x: None, set_passphrase)
295
296 @property
297 def activation_code(self):
298 return self.data.activation_code
299
300 def get_realname(self):
301 if not self.data.realname:
302 return self.name
303
304 return self.data.realname
305
306 def set_realname(self, realname):
307 self.db.execute("UPDATE users SET realname = %s WHERE id = %s",
308 realname, self.id)
309 self.data["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 firstname(self):
319 # Try to split the string into first and last name.
320 # If that is not successful, return the entire realname.
321 try:
322 firstname, rest = self.realname.split(" ", 1)
323 except:
324 return self.realname
325
326 return firstname
327
328 def get_email(self):
329 if self._emails is None:
330 self._emails = self.db.query("SELECT * FROM users_emails WHERE user_id = %s", self.id)
331 assert self._emails
332
333 for email in self._emails:
334 if not email.primary == "Y":
335 continue
336
337 return email.email
338
339 def set_email(self, email):
340 if email == self.email:
341 return
342
343 self.db.execute("UPDATE users_emails SET email = %s \
344 WHERE user_id = %s AND primary = 'Y'", email, self.id)
345
346 self.db.execute("UPDATE users SET activated 'N' WHERE id = %s",
347 email, self.id)
348
349 # Reset cache.
350 self._data = self._emails = None
351
352 # Inform the user, that he or she has to re-activate the account.
353 self.send_activation_mail()
354
355 email = property(get_email, set_email)
356
357 def get_state(self):
358 return self.data.state
359
360 def set_state(self, state):
361 self.db.execute("UPDATE users SET state = %s WHERE id = %s", state,
362 self.id)
363 self.data["state"] = state
364
365 state = property(get_state, set_state)
366
367 def get_locale(self):
368 return self.data.locale or ""
369
370 def set_locale(self, locale):
371 self.db.execute("UPDATE users SET locale = %s WHERE id = %s", locale,
372 self.id)
373 self.data["locale"] = locale
374
375 locale = property(get_locale, set_locale)
376
377 def get_timezone(self, tz=None):
378 if tz is None:
379 tz = self.data.timezone or ""
380
381 try:
382 tz = pytz.timezone(tz)
383 except pytz.UnknownTimeZoneError:
384 tz = pytz.timezone("UTC")
385
386 return tz
387
388 def set_timezone(self, timezone):
389 if not timezone is None:
390 tz = self.get_timezone(timezone)
391 timezone = tz.zone
392
393 self.db.execute("UPDATE users SET timezone = %s WHERE id = %s",
394 timezone, self.id)
395
396 timezone = property(get_timezone, set_timezone)
397
398 @property
399 def activated(self):
400 return self.data.activated == "Y"
401
402 @property
403 def registered(self):
404 return self.data.registered
405
406 def gravatar_icon(self, size=128):
407 # construct the url
408 gravatar_url = "http://www.gravatar.com/avatar/" + \
409 hashlib.md5(self.email.lower()).hexdigest() + "?"
410 gravatar_url += urllib.urlencode({'d': "mm", 's': str(size)})
411
412 return gravatar_url
413
414 def is_admin(self):
415 return self.state == "admin"
416
417 def is_tester(self):
418 return self.state == "tester"
419
420 @property
421 def perms(self):
422 if self._perms is None:
423 self._perms = \
424 self.db.get("SELECT * FROM users_permissions WHERE user_id = %s", self.id)
425
426 return self._perms
427
428 def has_perm(self, perm):
429 """
430 Returns True if the user has the requested permission.
431 """
432 # Admins have the permission for everything.
433 if self.is_admin():
434 return True
435
436 # Exception for voting. All testers are allowed to vote.
437 if perm == "vote" and self.is_tester():
438 return True
439
440 # All others must be checked individually.
441 return self.perms.get(perm, "N") == "Y"
442
443 def send_activation_mail(self):
444 logging.debug("Sending activation mail to %s" % self.email)
445
446 # Generate a random activation code.
447 source = string.ascii_letters + string.digits
448 self.data["activation_code"] = "".join(random.sample(source * 20, 20))
449 self.db.execute("UPDATE users SET activation_code = %s WHERE id = %s",
450 self.activation_code, self.id)
451
452 # Get the saved locale from the user.
453 locale = tornado.locale.get(self.locale)
454 _ = locale.translate
455
456 subject = _("Account Activation")
457
458 message = _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.")
459 message += "\n"*2
460 message += _("To activate your account, please click on the link below.")
461 message += "\n"*2
462 message += " %(baseurl)s/user/%(name)s/activate?code=%(activation_code)s" \
463 % { "baseurl" : self.settings.get("baseurl"), "name" : self.name,
464 "activation_code" : self.activation_code, }
465 message += "\n"*2
466 message += "Sincerely,\n The Pakfire Build Service"
467
468 self.pakfire.messages.add("%s <%s>" % (self.realname, self.email), subject, message)
469
470
471 # Some testing code.
472 if __name__ == "__main__":
473 for password in ("1234567890", "abcdefghij"):
474 digest = generate_password_hash(password)
475
476 print "%s %s" % (password, digest)
477 print " Matches? %s" % check_password_hash(password, digest)
478