]>
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 | ||
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 | ||
9137135a | 109 | class Users(base.Object): |
f6e6ff79 MT |
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): | |
9137135a MT |
113 | return |
114 | ||
f6e6ff79 MT |
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) | |
9137135a | 119 | |
f6e6ff79 MT |
120 | if not user: |
121 | return | |
9137135a | 122 | |
f6e6ff79 MT |
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) | |
9137135a MT |
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): | |
f6e6ff79 | 146 | users = self.db.query("SELECT id FROM users_emails WHERE email = %s", email) |
9137135a MT |
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 | |
3da206e1 | 155 | deleted = 'N' ORDER BY name ASC""") |
9137135a MT |
156 | |
157 | return [User(self.pakfire, u.id) for u in users] | |
158 | ||
159 | def get_by_id(self, id): | |
f6e6ff79 | 160 | return User(self.pakfire, id) |
9137135a MT |
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): | |
f6e6ff79 MT |
169 | user = self.db.get("SELECT user_id AS id FROM users_emails \ |
170 | WHERE email = %s LIMIT 1", email) | |
9137135a MT |
171 | |
172 | if user: | |
173 | return User(self.pakfire, user.id) | |
174 | ||
f6e6ff79 | 175 | def count(self): |
966498de MT |
176 | users = self.db.get("SELECT COUNT(*) AS count FROM users \ |
177 | WHERE activated = 'Y' AND deleted = 'N'") | |
f6e6ff79 | 178 | |
966498de MT |
179 | if users: |
180 | return users.count | |
f6e6ff79 MT |
181 | |
182 | def search(self, pattern, limit=None): | |
c9619eec MT |
183 | pattern = "%%%s%%" % pattern |
184 | ||
f6e6ff79 | 185 | query = "SELECT id FROM users \ |
c9619eec MT |
186 | WHERE (name LIKE %s OR realname LIKE %s) AND activated = %s AND deleted = %s" |
187 | args = [pattern, pattern, "Y", "N"] | |
f6e6ff79 MT |
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 | ||
9137135a MT |
214 | |
215 | class User(base.Object): | |
216 | def __init__(self, pakfire, id): | |
217 | base.Object.__init__(self, pakfire) | |
218 | self.id = id | |
219 | ||
f6e6ff79 MT |
220 | # Cache. |
221 | self._data = None | |
222 | self._emails = None | |
223 | self._perms = None | |
9137135a | 224 | |
20d7f5eb MT |
225 | def __repr__(self): |
226 | return "<%s %s>" % (self.__class__.__name__, self.realname) | |
227 | ||
228 | def __hash__(self): | |
229 | return hash(self.id) | |
230 | ||
9137135a | 231 | def __cmp__(self, other): |
f6e6ff79 MT |
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) | |
9137135a MT |
242 | |
243 | @classmethod | |
244 | def new(cls, pakfire, name, passphrase, email, realname, locale=None): | |
f6e6ff79 MT |
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) | |
9137135a | 251 | |
f6e6ff79 MT |
252 | # Create row in permissions table. |
253 | pakfire.db.execute("INSERT INTO users_permissions(user_id) VALUES(%s)", id) | |
9137135a MT |
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 | ||
f6e6ff79 MT |
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 | ||
9137135a MT |
273 | def delete(self): |
274 | self.db.execute("UPDATE users SET deleted = 'Y' WHERE id = %s", self.id) | |
f6e6ff79 | 275 | self._data = None |
9137135a MT |
276 | |
277 | def activate(self): | |
f6e6ff79 MT |
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) | |
9137135a MT |
286 | |
287 | def set_passphrase(self, passphrase): | |
288 | """ | |
289 | Update the passphrase the users uses to log on. | |
290 | """ | |
f6e6ff79 MT |
291 | self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s", |
292 | generate_password_hash(passphrase), self.id) | |
9137135a MT |
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 | ||
f6e6ff79 MT |
317 | @property |
318 | def firstname(self): | |
3f82e940 MT |
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 | |
f6e6ff79 MT |
325 | |
326 | return firstname | |
327 | ||
9137135a | 328 | def get_email(self): |
f6e6ff79 MT |
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 | |
9137135a MT |
338 | |
339 | def set_email(self, email): | |
340 | if email == self.email: | |
341 | return | |
342 | ||
f6e6ff79 MT |
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) | |
9137135a | 348 | |
f6e6ff79 MT |
349 | # Reset cache. |
350 | self._data = self._emails = None | |
9137135a MT |
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 | ||
f6e6ff79 MT |
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 | ||
9137135a MT |
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 | ||
f6e6ff79 MT |
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 | ||
9137135a MT |
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 | ||
c6009001 | 458 | message = _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.") |
9137135a MT |
459 | message += "\n"*2 |
460 | message += _("To activate your account, please click on the link below.") | |
461 | message += "\n"*2 | |
f6e6ff79 MT |
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, } | |
9137135a MT |
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 | ||
f6e6ff79 MT |
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) | |
9137135a | 478 |