]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
people: Check StopForumSpam when registering accounts
[ipfire.org.git] / src / backend / accounts.py
1 #!/usr/bin/python
2 # encoding: utf-8
3
4 import datetime
5 import json
6 import ldap
7 import ldap.modlist
8 import logging
9 import os
10 import phonenumbers
11 import sshpubkeys
12 import time
13 import tornado.httpclient
14 import urllib.parse
15 import urllib.request
16 import zxcvbn
17
18 from . import countries
19 from . import util
20 from .decorators import *
21 from .misc import Object
22
23 # Set the client keytab name
24 os.environ["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
25
26 class Accounts(Object):
27 def init(self):
28 self.search_base = self.settings.get("ldap_search_base")
29
30 def __iter__(self):
31 # Only return developers (group with ID 1000)
32 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
33
34 return iter(sorted(accounts))
35
36 @lazy_property
37 def ldap(self):
38 # Connect to LDAP server
39 ldap_uri = self.settings.get("ldap_uri")
40
41 logging.debug("Connecting to LDAP server: %s" % ldap_uri)
42
43 # Connect to the LDAP server
44 return ldap.ldapobject.ReconnectLDAPObject(ldap_uri,
45 retry_max=10, retry_delay=3)
46
47 def _authenticate(self):
48 # Authenticate against LDAP server using Kerberos
49 self.ldap.sasl_gssapi_bind_s()
50
51 def test_ldap(self):
52 logging.info("Testing LDAP connection...")
53
54 self._authenticate()
55
56 logging.info("Successfully authenticated as %s" % self.ldap.whoami_s())
57
58 def _query(self, query, attrlist=None, limit=0, search_base=None):
59 logging.debug("Performing LDAP query (%s): %s" \
60 % (search_base or self.search_base, query))
61
62 t = time.time()
63
64 results = self.ldap.search_ext_s(search_base or self.search_base,
65 ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit)
66
67 # Log time it took to perform the query
68 logging.debug("Query took %.2fms" % ((time.time() - t) * 1000.0))
69
70 return results
71
72 def _search(self, query, attrlist=None, limit=0):
73 accounts = []
74 for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
75 account = self.get_by_dn(dn)
76 accounts.append(account)
77
78 return accounts
79
80 def _get_attrs(self, dn):
81 """
82 Fetches all attributes for the given distinguished name
83 """
84 results = self._query("(objectClass=*)", search_base=dn, limit=1,
85 attrlist=("*", "createTimestamp", "modifyTimestamp"))
86
87 for dn, attrs in results:
88 return attrs
89
90 def get_by_dn(self, dn):
91 attrs = self.memcache.get("accounts:%s:attrs" % dn)
92 if attrs is None:
93 attrs = self._get_attrs(dn)
94 assert attrs, dn
95
96 # Cache all attributes for 5 min
97 self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
98
99 return Account(self.backend, dn, attrs)
100
101 def get_created_after(self, ts):
102 t = ts.strftime("%Y%m%d%H%M%SZ")
103
104 return self._search("(&(objectClass=person)(createTimestamp>=%s))" % t)
105
106 def search(self, query):
107 # Search for exact matches
108 accounts = self._search(
109 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
110 % (query, query, query, query, query, query))
111
112 # Find accounts by name
113 if not accounts:
114 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
115 if not account in accounts:
116 accounts.append(account)
117
118 return sorted(accounts)
119
120 def _search_one(self, query):
121 results = self._search(query, limit=1)
122
123 for result in results:
124 return result
125
126 def uid_exists(self, uid):
127 if self.get_by_uid(uid):
128 return True
129
130 res = self.db.get("SELECT 1 FROM account_activations \
131 WHERE uid = %s AND expires_at > NOW()", uid)
132
133 if res:
134 return True
135
136 # Account with uid does not exist, yet
137 return False
138
139 def get_by_uid(self, uid):
140 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
141
142 def get_by_mail(self, mail):
143 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
144
145 def find_account(self, s):
146 account = self.get_by_uid(s)
147 if account:
148 return account
149
150 return self.get_by_mail(s)
151
152 def get_by_sip_id(self, sip_id):
153 if not sip_id:
154 return
155
156 return self._search_one(
157 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
158 % (sip_id, sip_id))
159
160 def get_by_phone_number(self, number):
161 if not number:
162 return
163
164 return self._search_one(
165 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
166 % (number, number, number, number))
167
168 @tornado.gen.coroutine
169 def check_spam(self, uid, email, address):
170 sfs = StopForumSpam(self.backend, uid, email, address)
171
172 # Get spam score
173 score = yield sfs.check()
174
175 return score >= 50
176
177 # Registration
178
179 def register(self, uid, email, first_name, last_name):
180 # Convert all uids to lowercase
181 uid = uid.lower()
182
183 # Check if UID is unique
184 if self.uid_exists(uid):
185 raise ValueError("UID exists: %s" % uid)
186
187 # Generate a random activation code
188 activation_code = util.random_string(36)
189
190 # Create an entry in our database until the user
191 # has activated the account
192 self.db.execute("INSERT INTO account_activations(uid, activation_code, \
193 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
194 uid, activation_code, email, first_name, last_name)
195
196 # Send an account activation email
197 self.backend.messages.send_template("auth/messages/register",
198 recipients=[email], priority=100, uid=uid,
199 activation_code=activation_code, email=email,
200 first_name=first_name, last_name=last_name)
201
202 def activate(self, uid, activation_code):
203 res = self.db.get("DELETE FROM account_activations \
204 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
205 RETURNING *", uid, activation_code)
206
207 # Return nothing when account was not found
208 if not res:
209 return
210
211 # Create a new account on the LDAP database
212 return self.create(uid, res.email,
213 first_name=res.first_name, last_name=res.last_name)
214
215 def create(self, uid, email, first_name, last_name):
216 cn = "%s %s" % (first_name, last_name)
217
218 # Account Parameters
219 account = {
220 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
221 "mail" : email.encode(),
222
223 # Name
224 "cn" : cn.encode(),
225 "sn" : last_name.encode(),
226 "givenName" : first_name.encode(),
227 }
228
229 logging.info("Creating new account: %s: %s" % (uid, account))
230
231 # Create DN
232 dn = "uid=%s,ou=People,dc=ipfire,dc=org" % uid
233
234 # Create account on LDAP
235 self.accounts._authenticate()
236 self.ldap.add_s(dn, ldap.modlist.addModlist(account))
237
238 # Return account
239 return self.get_by_dn(dn)
240
241 # Session stuff
242
243 def create_session(self, account, host):
244 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
245 RETURNING session_id, time_expires", host, account.uid)
246
247 # Session could not be created
248 if not res:
249 return None, None
250
251 logging.info("Created session %s for %s which expires %s" \
252 % (res.session_id, account, res.time_expires))
253 return res.session_id, res.time_expires
254
255 def destroy_session(self, session_id, host):
256 logging.info("Destroying session %s" % session_id)
257
258 self.db.execute("DELETE FROM sessions \
259 WHERE session_id = %s AND host = %s", session_id, host)
260
261 def get_by_session(self, session_id, host):
262 logging.debug("Looking up session %s" % session_id)
263
264 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
265 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
266 session_id, host)
267
268 # Session does not exist or has expired
269 if not res:
270 return
271
272 # Update the session expiration time
273 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
274 WHERE session_id = %s AND host = %s", session_id, host)
275
276 return self.get_by_uid(res.uid)
277
278 def cleanup(self):
279 # Cleanup expired sessions
280 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
281
282 # Cleanup expired account activations
283 self.db.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
284
285
286 class Account(Object):
287 def __init__(self, backend, dn, attrs=None):
288 Object.__init__(self, backend)
289 self.dn = dn
290
291 self.attributes = attrs or {}
292
293 def __str__(self):
294 if self.nickname:
295 return self.nickname
296
297 return self.name
298
299 def __repr__(self):
300 return "<%s %s>" % (self.__class__.__name__, self.dn)
301
302 def __eq__(self, other):
303 if isinstance(other, self.__class__):
304 return self.dn == other.dn
305
306 def __lt__(self, other):
307 if isinstance(other, self.__class__):
308 return self.name < other.name
309
310 @property
311 def ldap(self):
312 return self.accounts.ldap
313
314 def _exists(self, key):
315 try:
316 self.attributes[key]
317 except KeyError:
318 return False
319
320 return True
321
322 def _get(self, key):
323 for value in self.attributes.get(key, []):
324 yield value
325
326 def _get_bytes(self, key, default=None):
327 for value in self._get(key):
328 return value
329
330 return default
331
332 def _get_strings(self, key):
333 for value in self._get(key):
334 yield value.decode()
335
336 def _get_string(self, key, default=None):
337 for value in self._get_strings(key):
338 return value
339
340 return default
341
342 def _get_phone_numbers(self, key):
343 for value in self._get_strings(key):
344 yield phonenumbers.parse(value, None)
345
346 def _get_timestamp(self, key):
347 value = self._get_string(key)
348
349 # Parse the timestamp value and returns a datetime object
350 if value:
351 return datetime.datetime.strptime(value, "%Y%m%d%H%M%SZ")
352
353 def _modify(self, modlist):
354 logging.debug("Modifying %s: %s" % (self.dn, modlist))
355
356 # Authenticate before performing any write operations
357 self.accounts._authenticate()
358
359 # Run modify operation
360 self.ldap.modify_s(self.dn, modlist)
361
362 # Delete cached attributes
363 self.memcache.delete("accounts:%s:attrs" % self.dn)
364
365 def _set(self, key, values):
366 current = self._get(key)
367
368 # Don't do anything if nothing has changed
369 if list(current) == values:
370 return
371
372 # Remove all old values and add all new ones
373 modlist = []
374
375 if self._exists(key):
376 modlist.append((ldap.MOD_DELETE, key, None))
377
378 # Add new values
379 if values:
380 modlist.append((ldap.MOD_ADD, key, values))
381
382 # Run modify operation
383 self._modify(modlist)
384
385 # Update cache
386 self.attributes.update({ key : values })
387
388 def _set_bytes(self, key, values):
389 return self._set(key, values)
390
391 def _set_strings(self, key, values):
392 return self._set(key, [e.encode() for e in values if e])
393
394 def _set_string(self, key, value):
395 return self._set_strings(key, [value,])
396
397 def _add(self, key, values):
398 modlist = [
399 (ldap.MOD_ADD, key, values),
400 ]
401
402 self._modify(modlist)
403
404 def _add_strings(self, key, values):
405 return self._add(key, [e.encode() for e in values])
406
407 def _add_string(self, key, value):
408 return self._add_strings(key, [value,])
409
410 def _delete(self, key, values):
411 modlist = [
412 (ldap.MOD_DELETE, key, values),
413 ]
414
415 self._modify(modlist)
416
417 def _delete_strings(self, key, values):
418 return self._delete(key, [e.encode() for e in values])
419
420 def _delete_string(self, key, value):
421 return self._delete_strings(key, [value,])
422
423 def passwd(self, password):
424 """
425 Sets a new password
426 """
427 # The new password must have a score of 3 or better
428 quality = self.check_password_quality(password)
429 if quality["score"] < 3:
430 raise ValueError("Password too weak")
431
432 self.accounts._authenticate()
433 self.ldap.passwd_s(self.dn, None, password)
434
435 def check_password(self, password):
436 """
437 Bind to the server with given credentials and return
438 true if password is corrent and false if not.
439
440 Raises exceptions from the server on any other errors.
441 """
442 if not password:
443 return
444
445 logging.debug("Checking credentials for %s" % self.dn)
446
447 # Create a new LDAP connection
448 ldap_uri = self.backend.settings.get("ldap_uri")
449 conn = ldap.initialize(ldap_uri)
450
451 try:
452 conn.simple_bind_s(self.dn, password.encode("utf-8"))
453 except ldap.INVALID_CREDENTIALS:
454 logging.debug("Account credentials are invalid for %s" % self)
455 return False
456
457 logging.info("Successfully authenticated %s" % self)
458
459 return True
460
461 def check_password_quality(self, password):
462 """
463 Passwords are passed through zxcvbn to make sure
464 that they are strong enough.
465 """
466 return zxcvbn.zxcvbn(password, user_inputs=(
467 self.first_name, self.last_name,
468 ))
469
470 def is_admin(self):
471 return "wheel" in self.groups
472
473 def is_staff(self):
474 return "staff" in self.groups
475
476 def has_shell(self):
477 return "posixAccount" in self.classes
478
479 def has_mail(self):
480 return "postfixMailUser" in self.classes
481
482 def has_sip(self):
483 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
484
485 def can_be_managed_by(self, account):
486 """
487 Returns True if account is allowed to manage this account
488 """
489 # Admins can manage all accounts
490 if account.is_admin():
491 return True
492
493 # Users can manage themselves
494 return self == account
495
496 @property
497 def classes(self):
498 return self._get_strings("objectClass")
499
500 @property
501 def uid(self):
502 return self._get_string("uid")
503
504 @property
505 def name(self):
506 return self._get_string("cn")
507
508 # Nickname
509
510 def get_nickname(self):
511 return self._get_string("displayName")
512
513 def set_nickname(self, nickname):
514 self._set_string("displayName", nickname)
515
516 nickname = property(get_nickname, set_nickname)
517
518 # First Name
519
520 def get_first_name(self):
521 return self._get_string("givenName")
522
523 def set_first_name(self, first_name):
524 self._set_string("givenName", first_name)
525
526 # Update Common Name
527 self._set_string("cn", "%s %s" % (first_name, self.last_name))
528
529 first_name = property(get_first_name, set_first_name)
530
531 # Last Name
532
533 def get_last_name(self):
534 return self._get_string("sn")
535
536 def set_last_name(self, last_name):
537 self._set_string("sn", last_name)
538
539 # Update Common Name
540 self._set_string("cn", "%s %s" % (self.first_name, last_name))
541
542 last_name = property(get_last_name, set_last_name)
543
544 @lazy_property
545 def groups(self):
546 groups = self.memcache.get("accounts:%s:groups" % self.dn)
547 if groups:
548 return groups
549
550 # Fetch groups from LDAP
551 groups = self._get_groups()
552
553 # Cache groups for 5 min
554 self.memcache.set("accounts:%s:groups" % self.dn, groups, 300)
555
556 return groups
557
558 def _get_groups(self):
559 groups = []
560
561 res = self.accounts._query("(&(objectClass=posixGroup) \
562 (memberUid=%s))" % self.uid, ["cn"])
563
564 for dn, attrs in res:
565 cns = attrs.get("cn")
566 if cns:
567 groups.append(cns[0].decode())
568
569 return groups
570
571 # Created/Modified at
572
573 @property
574 def created_at(self):
575 return self._get_timestamp("createTimestamp")
576
577 @property
578 def modified_at(self):
579 return self._get_timestamp("modifyTimestamp")
580
581 # Address
582
583 @property
584 def address(self):
585 address = []
586
587 if self.street:
588 address += self.street.splitlines()
589
590 if self.postal_code and self.city:
591 if self.country_code in ("AT", "DE"):
592 address.append("%s %s" % (self.postal_code, self.city))
593 else:
594 address.append("%s, %s" % (self.city, self.postal_code))
595 else:
596 address.append(self.city or self.postal_code)
597
598 if self.country_name:
599 address.append(self.country_name)
600
601 return address
602
603 def get_street(self):
604 return self._get_string("street") or self._get_string("homePostalAddress")
605
606 def set_street(self, street):
607 self._set_string("street", street)
608
609 street = property(get_street, set_street)
610
611 def get_city(self):
612 return self._get_string("l") or ""
613
614 def set_city(self, city):
615 self._set_string("l", city)
616
617 city = property(get_city, set_city)
618
619 def get_postal_code(self):
620 return self._get_string("postalCode") or ""
621
622 def set_postal_code(self, postal_code):
623 self._set_string("postalCode", postal_code)
624
625 postal_code = property(get_postal_code, set_postal_code)
626
627 # XXX This should be c
628 def get_country_code(self):
629 return self._get_string("st")
630
631 def set_country_code(self, country_code):
632 self._set_string("st", country_code)
633
634 country_code = property(get_country_code, set_country_code)
635
636 @property
637 def country_name(self):
638 if self.country_code:
639 return countries.get_name(self.country_code)
640
641 @property
642 def email(self):
643 return self._get_string("mail")
644
645 # Mail Routing Address
646
647 def get_mail_routing_address(self):
648 return self._get_string("mailRoutingAddress", None)
649
650 def set_mail_routing_address(self, address):
651 self._set_string("mailRoutingAddress", address or None)
652
653 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
654
655 @property
656 def sip_id(self):
657 if "sipUser" in self.classes:
658 return self._get_string("sipAuthenticationUser")
659
660 if "sipRoutingObject" in self.classes:
661 return self._get_string("sipLocalAddress")
662
663 @property
664 def sip_password(self):
665 return self._get_string("sipPassword")
666
667 @staticmethod
668 def _generate_sip_password():
669 return util.random_string(8)
670
671 @property
672 def sip_url(self):
673 return "%s@ipfire.org" % self.sip_id
674
675 def uses_sip_forwarding(self):
676 if self.sip_routing_address:
677 return True
678
679 return False
680
681 # SIP Routing
682
683 def get_sip_routing_address(self):
684 if "sipRoutingObject" in self.classes:
685 return self._get_string("sipRoutingAddress")
686
687 def set_sip_routing_address(self, address):
688 if not address:
689 address = None
690
691 # Don't do anything if nothing has changed
692 if self.get_sip_routing_address() == address:
693 return
694
695 if address:
696 # This is no longer a SIP user any more
697 try:
698 self._modify([
699 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
700 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
701 (ldap.MOD_DELETE, "sipPassword", None),
702 ])
703 except ldap.NO_SUCH_ATTRIBUTE:
704 pass
705
706 # Set new routing object
707 try:
708 self._modify([
709 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
710 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
711 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
712 ])
713
714 # If this is a change, we cannot add this again
715 except ldap.TYPE_OR_VALUE_EXISTS:
716 self._set_string("sipRoutingAddress", address)
717 else:
718 try:
719 self._modify([
720 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
721 (ldap.MOD_DELETE, "sipLocalAddress", None),
722 (ldap.MOD_DELETE, "sipRoutingAddress", None),
723 ])
724 except ldap.NO_SUCH_ATTRIBUTE:
725 pass
726
727 self._modify([
728 (ldap.MOD_ADD, "objectClass", b"sipUser"),
729 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
730 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
731 ])
732
733 # XXX Cache is invalid here
734
735 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
736
737 @lazy_property
738 def sip_registrations(self):
739 sip_registrations = []
740
741 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
742 reg.account = self
743
744 sip_registrations.append(reg)
745
746 return sip_registrations
747
748 @lazy_property
749 def sip_channels(self):
750 return self.backend.talk.freeswitch.get_sip_channels(self)
751
752 def get_cdr(self, date=None, limit=None):
753 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
754
755 # Phone Numbers
756
757 @lazy_property
758 def phone_number(self):
759 """
760 Returns the IPFire phone number
761 """
762 if self.sip_id:
763 return phonenumbers.parse("+4923636035%s" % self.sip_id)
764
765 @lazy_property
766 def fax_number(self):
767 if self.sip_id:
768 return phonenumbers.parse("+49236360359%s" % self.sip_id)
769
770 def get_phone_numbers(self):
771 ret = []
772
773 for field in ("telephoneNumber", "homePhone", "mobile"):
774 for number in self._get_phone_numbers(field):
775 ret.append(number)
776
777 return ret
778
779 def set_phone_numbers(self, phone_numbers):
780 # Sort phone numbers by landline and mobile
781 _landline_numbers = []
782 _mobile_numbers = []
783
784 for number in phone_numbers:
785 try:
786 number = phonenumbers.parse(number, None)
787 except phonenumbers.phonenumberutil.NumberParseException:
788 continue
789
790 # Convert to string (in E.164 format)
791 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
792
793 # Separate mobile numbers
794 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
795 _mobile_numbers.append(s)
796 else:
797 _landline_numbers.append(s)
798
799 # Save
800 self._set_strings("telephoneNumber", _landline_numbers)
801 self._set_strings("mobile", _mobile_numbers)
802
803 phone_numbers = property(get_phone_numbers, set_phone_numbers)
804
805 @property
806 def _all_telephone_numbers(self):
807 ret = [ self.sip_id, ]
808
809 if self.phone_number:
810 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
811 ret.append(s)
812
813 for number in self.phone_numbers:
814 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
815 ret.append(s)
816
817 return ret
818
819 def avatar_url(self, size=None):
820 if self.backend.debug:
821 hostname = "http://people.dev.ipfire.org"
822 else:
823 hostname = "https://people.ipfire.org"
824
825 url = "%s/users/%s.jpg" % (hostname, self.uid)
826
827 if size:
828 url += "?size=%s" % size
829
830 return url
831
832 def get_avatar(self, size=None):
833 photo = self._get_bytes("jpegPhoto")
834
835 # Exit if no avatar is available
836 if not photo:
837 return
838
839 # Return the raw image if no size was requested
840 if size is None:
841 return photo
842
843 # Try to retrieve something from the cache
844 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
845 if avatar:
846 return avatar
847
848 # Generate a new thumbnail
849 avatar = util.generate_thumbnail(photo, size, square=True)
850
851 # Save to cache for 15m
852 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
853
854 return avatar
855
856 def upload_avatar(self, avatar):
857 self._set("jpegPhoto", avatar)
858
859 # SSH Keys
860
861 @lazy_property
862 def ssh_keys(self):
863 ret = []
864
865 for key in self._get_strings("sshPublicKey"):
866 s = sshpubkeys.SSHKey()
867
868 try:
869 s.parse(key)
870 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
871 logging.warning("Could not parse SSH key %s: %s" % (key, e))
872 continue
873
874 ret.append(s)
875
876 return ret
877
878 def get_ssh_key_by_hash_sha256(self, hash_sha256):
879 for key in self.ssh_keys:
880 if not key.hash_sha256() == hash_sha256:
881 continue
882
883 return key
884
885 def add_ssh_key(self, key):
886 k = sshpubkeys.SSHKey()
887
888 # Try to parse the key
889 k.parse(key)
890
891 # Check for types and sufficient sizes
892 if k.key_type == b"ssh-rsa":
893 if k.bits < 4096:
894 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
895
896 elif k.key_type == b"ssh-dss":
897 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
898
899 # Ignore any duplicates
900 if key in (k.keydata for k in self.ssh_keys):
901 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
902 return
903
904 # Prepare transaction
905 modlist = []
906
907 # Add object class if user is not in it, yet
908 if not "ldapPublicKey" in self.classes:
909 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
910
911 # Add key
912 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
913
914 # Save key to LDAP
915 self._modify(modlist)
916
917 # Append to cache
918 self.ssh_keys.append(k)
919
920 def delete_ssh_key(self, key):
921 if not key in (k.keydata for k in self.ssh_keys):
922 return
923
924 # Delete key from LDAP
925 if len(self.ssh_keys) > 1:
926 self._delete_string("sshPublicKey", key)
927 else:
928 self._modify([
929 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
930 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
931 ])
932
933
934 class StopForumSpam(Object):
935 def init(self, uid, email, address):
936 self.uid, self.email, self.address = uid, email, address
937
938 @tornado.gen.coroutine
939 def send_request(self, **kwargs):
940 arguments = {
941 "json" : "1",
942 }
943 arguments.update(kwargs)
944
945 # Create request
946 request = tornado.httpclient.HTTPRequest(
947 "https://api.stopforumspam.org/api", method="POST")
948 request.body = urllib.parse.urlencode(arguments)
949
950 # Send the request
951 response = yield self.backend.http_client.fetch(request)
952
953 # Decode the JSON response
954 return json.loads(response.body.decode())
955
956 @tornado.gen.coroutine
957 def check_address(self):
958 response = yield self.send_request(ip=self.address)
959
960 try:
961 confidence = response["ip"]["confidence"]
962 except KeyError:
963 confidence = 100
964
965 logging.debug("Confidence for %s: %s" % (self.address, confidence))
966
967 return confidence
968
969 @tornado.gen.coroutine
970 def check_username(self):
971 response = yield self.send_request(username=self.uid)
972
973 try:
974 confidence = response["username"]["confidence"]
975 except KeyError:
976 confidence = 100
977
978 logging.debug("Confidence for %s: %s" % (self.uid, confidence))
979
980 return confidence
981
982 @tornado.gen.coroutine
983 def check_email(self):
984 response = yield self.send_request(email=self.email)
985
986 try:
987 confidence = response["email"]["confidence"]
988 except KeyError:
989 confidence = 100
990
991 logging.debug("Confidence for %s: %s" % (self.email, confidence))
992
993 return confidence
994
995 @tornado.gen.coroutine
996 def check(self, threshold=95):
997 """
998 This function tries to detect if we have a spammer.
999
1000 To honour the privacy of our users, we only send the IP
1001 address and username and if those are on the database, we
1002 will send the email address as well.
1003 """
1004 confidences = yield [self.check_address(), self.check_username()]
1005
1006 if any((c < threshold for c in confidences)):
1007 confidences += yield [self.check_email()]
1008
1009 # Build a score based on the lowest confidence
1010 return 100 - min(confidences)
1011
1012
1013 if __name__ == "__main__":
1014 a = Accounts()
1015
1016 print(a.list())