]>
Commit | Line | Data |
---|---|---|
9137135a MT |
1 | #!/usr/bin/python |
2 | ||
3 | import hashlib | |
4 | import logging | |
f6e6ff79 | 5 | import pytz |
9137135a | 6 | import random |
f6e6ff79 | 7 | import re |
9137135a MT |
8 | import string |
9 | import urllib | |
10 | ||
11 | import tornado.locale | |
12 | ||
2c909128 | 13 | from . import base |
9137135a | 14 | |
f6e6ff79 MT |
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 | ||
f6e6ff79 MT |
63 | def 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 | 72 | class 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 | |
218 | class 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. | |
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) | |
9137135a | 481 |