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