]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
c310cf5300f8de86b9525016c2fb3890cf87470f
[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 @lazy_property
489 def kerberos_attributes(self):
490 res = self.backend.accounts._query(
491 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self.uid,
492 attrlist=[
493 "krbLastSuccessfulAuth",
494 "krbLastPasswordChange",
495 "krbLastFailedAuth",
496 "krbLoginFailedCount",
497 ],
498 limit=1,
499 search_base="cn=krb5,%s" % self.backend.accounts.search_base)
500
501 for dn, attrs in res:
502 return { key : attrs[key][0] for key in attrs }
503
504 return {}
505
506 @staticmethod
507 def _parse_date(s):
508 return datetime.datetime.strptime(s.decode(), "%Y%m%d%H%M%SZ")
509
510 @property
511 def last_successful_authentication(self):
512 try:
513 s = self.kerberos_attributes["krbLastSuccessfulAuth"]
514 except KeyError:
515 return None
516
517 return self._parse_date(s)
518
519 @property
520 def last_failed_authentication(self):
521 try:
522 s = self.kerberos_attributes["krbLastFailedAuth"]
523 except KeyError:
524 return None
525
526 return self._parse_date(s)
527
528 @property
529 def failed_login_count(self):
530 try:
531 count = self.kerberos_attributes["krbLoginFailedCount"].decode()
532 except KeyError:
533 return 0
534
535 try:
536 return int(count)
537 except ValueError:
538 return 0
539
540 def passwd(self, password):
541 """
542 Sets a new password
543 """
544 # The new password must have a score of 3 or better
545 quality = self.check_password_quality(password)
546 if quality["score"] < 3:
547 raise ValueError("Password too weak")
548
549 self.accounts._authenticate()
550 self.ldap.passwd_s(self.dn, None, password)
551
552 def check_password(self, password):
553 """
554 Bind to the server with given credentials and return
555 true if password is corrent and false if not.
556
557 Raises exceptions from the server on any other errors.
558 """
559 if not password:
560 return
561
562 logging.debug("Checking credentials for %s" % self.dn)
563
564 # Create a new LDAP connection
565 ldap_uri = self.backend.settings.get("ldap_uri")
566 conn = ldap.initialize(ldap_uri)
567
568 try:
569 conn.simple_bind_s(self.dn, password.encode("utf-8"))
570 except ldap.INVALID_CREDENTIALS:
571 logging.debug("Account credentials are invalid for %s" % self)
572 return False
573
574 logging.info("Successfully authenticated %s" % self)
575
576 return True
577
578 def check_password_quality(self, password):
579 """
580 Passwords are passed through zxcvbn to make sure
581 that they are strong enough.
582 """
583 return zxcvbn.zxcvbn(password, user_inputs=(
584 self.first_name, self.last_name,
585 ))
586
587 def is_admin(self):
588 return "sudo" in self.groups
589
590 def is_staff(self):
591 return "staff" in self.groups
592
593 def has_shell(self):
594 return "posixAccount" in self.classes
595
596 def has_mail(self):
597 return "postfixMailUser" in self.classes
598
599 def has_sip(self):
600 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
601
602 def can_be_managed_by(self, account):
603 """
604 Returns True if account is allowed to manage this account
605 """
606 # Admins can manage all accounts
607 if account.is_admin():
608 return True
609
610 # Users can manage themselves
611 return self == account
612
613 @property
614 def classes(self):
615 return self._get_strings("objectClass")
616
617 @property
618 def uid(self):
619 return self._get_string("uid")
620
621 @property
622 def name(self):
623 return self._get_string("cn")
624
625 # Nickname
626
627 def get_nickname(self):
628 return self._get_string("displayName")
629
630 def set_nickname(self, nickname):
631 self._set_string("displayName", nickname)
632
633 nickname = property(get_nickname, set_nickname)
634
635 # First Name
636
637 def get_first_name(self):
638 return self._get_string("givenName")
639
640 def set_first_name(self, first_name):
641 self._set_string("givenName", first_name)
642
643 # Update Common Name
644 self._set_string("cn", "%s %s" % (first_name, self.last_name))
645
646 first_name = property(get_first_name, set_first_name)
647
648 # Last Name
649
650 def get_last_name(self):
651 return self._get_string("sn")
652
653 def set_last_name(self, last_name):
654 self._set_string("sn", last_name)
655
656 # Update Common Name
657 self._set_string("cn", "%s %s" % (self.first_name, last_name))
658
659 last_name = property(get_last_name, set_last_name)
660
661 @lazy_property
662 def groups(self):
663 groups = self.memcache.get("accounts:%s:groups" % self.dn)
664 if groups:
665 return groups
666
667 # Fetch groups from LDAP
668 groups = self._get_groups()
669
670 # Cache groups for 5 min
671 self.memcache.set("accounts:%s:groups" % self.dn, groups, 300)
672
673 return groups
674
675 def _get_groups(self):
676 groups = []
677
678 res = self.accounts._query("(&(objectClass=posixGroup) \
679 (memberUid=%s))" % self.uid, ["cn"])
680
681 for dn, attrs in res:
682 cns = attrs.get("cn")
683 if cns:
684 groups.append(cns[0].decode())
685
686 return groups
687
688 # Created/Modified at
689
690 @property
691 def created_at(self):
692 return self._get_timestamp("createTimestamp")
693
694 @property
695 def modified_at(self):
696 return self._get_timestamp("modifyTimestamp")
697
698 # Address
699
700 @property
701 def address(self):
702 address = []
703
704 if self.street:
705 address += self.street.splitlines()
706
707 if self.postal_code and self.city:
708 if self.country_code in ("AT", "DE"):
709 address.append("%s %s" % (self.postal_code, self.city))
710 else:
711 address.append("%s, %s" % (self.city, self.postal_code))
712 else:
713 address.append(self.city or self.postal_code)
714
715 if self.country_name:
716 address.append(self.country_name)
717
718 return address
719
720 def get_street(self):
721 return self._get_string("street") or self._get_string("homePostalAddress")
722
723 def set_street(self, street):
724 self._set_string("street", street)
725
726 street = property(get_street, set_street)
727
728 def get_city(self):
729 return self._get_string("l") or ""
730
731 def set_city(self, city):
732 self._set_string("l", city)
733
734 city = property(get_city, set_city)
735
736 def get_postal_code(self):
737 return self._get_string("postalCode") or ""
738
739 def set_postal_code(self, postal_code):
740 self._set_string("postalCode", postal_code)
741
742 postal_code = property(get_postal_code, set_postal_code)
743
744 # XXX This should be c
745 def get_country_code(self):
746 return self._get_string("st")
747
748 def set_country_code(self, country_code):
749 self._set_string("st", country_code)
750
751 country_code = property(get_country_code, set_country_code)
752
753 @property
754 def country_name(self):
755 if self.country_code:
756 return countries.get_name(self.country_code)
757
758 @property
759 def email(self):
760 return self._get_string("mail")
761
762 # Mail Routing Address
763
764 def get_mail_routing_address(self):
765 return self._get_string("mailRoutingAddress", None)
766
767 def set_mail_routing_address(self, address):
768 self._set_string("mailRoutingAddress", address or None)
769
770 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
771
772 @property
773 def sip_id(self):
774 if "sipUser" in self.classes:
775 return self._get_string("sipAuthenticationUser")
776
777 if "sipRoutingObject" in self.classes:
778 return self._get_string("sipLocalAddress")
779
780 @property
781 def sip_password(self):
782 return self._get_string("sipPassword")
783
784 @staticmethod
785 def _generate_sip_password():
786 return util.random_string(8)
787
788 @property
789 def sip_url(self):
790 return "%s@ipfire.org" % self.sip_id
791
792 def uses_sip_forwarding(self):
793 if self.sip_routing_address:
794 return True
795
796 return False
797
798 # SIP Routing
799
800 def get_sip_routing_address(self):
801 if "sipRoutingObject" in self.classes:
802 return self._get_string("sipRoutingAddress")
803
804 def set_sip_routing_address(self, address):
805 if not address:
806 address = None
807
808 # Don't do anything if nothing has changed
809 if self.get_sip_routing_address() == address:
810 return
811
812 if address:
813 # This is no longer a SIP user any more
814 try:
815 self._modify([
816 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
817 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
818 (ldap.MOD_DELETE, "sipPassword", None),
819 ])
820 except ldap.NO_SUCH_ATTRIBUTE:
821 pass
822
823 # Set new routing object
824 try:
825 self._modify([
826 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
827 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
828 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
829 ])
830
831 # If this is a change, we cannot add this again
832 except ldap.TYPE_OR_VALUE_EXISTS:
833 self._set_string("sipRoutingAddress", address)
834 else:
835 try:
836 self._modify([
837 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
838 (ldap.MOD_DELETE, "sipLocalAddress", None),
839 (ldap.MOD_DELETE, "sipRoutingAddress", None),
840 ])
841 except ldap.NO_SUCH_ATTRIBUTE:
842 pass
843
844 self._modify([
845 (ldap.MOD_ADD, "objectClass", b"sipUser"),
846 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
847 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
848 ])
849
850 # XXX Cache is invalid here
851
852 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
853
854 @lazy_property
855 def sip_registrations(self):
856 sip_registrations = []
857
858 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
859 reg.account = self
860
861 sip_registrations.append(reg)
862
863 return sip_registrations
864
865 @lazy_property
866 def sip_channels(self):
867 return self.backend.talk.freeswitch.get_sip_channels(self)
868
869 def get_cdr(self, date=None, limit=None):
870 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
871
872 # Phone Numbers
873
874 @lazy_property
875 def phone_number(self):
876 """
877 Returns the IPFire phone number
878 """
879 if self.sip_id:
880 return phonenumbers.parse("+4923636035%s" % self.sip_id)
881
882 @lazy_property
883 def fax_number(self):
884 if self.sip_id:
885 return phonenumbers.parse("+49236360359%s" % self.sip_id)
886
887 def get_phone_numbers(self):
888 ret = []
889
890 for field in ("telephoneNumber", "homePhone", "mobile"):
891 for number in self._get_phone_numbers(field):
892 ret.append(number)
893
894 return ret
895
896 def set_phone_numbers(self, phone_numbers):
897 # Sort phone numbers by landline and mobile
898 _landline_numbers = []
899 _mobile_numbers = []
900
901 for number in phone_numbers:
902 try:
903 number = phonenumbers.parse(number, None)
904 except phonenumbers.phonenumberutil.NumberParseException:
905 continue
906
907 # Convert to string (in E.164 format)
908 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
909
910 # Separate mobile numbers
911 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
912 _mobile_numbers.append(s)
913 else:
914 _landline_numbers.append(s)
915
916 # Save
917 self._set_strings("telephoneNumber", _landline_numbers)
918 self._set_strings("mobile", _mobile_numbers)
919
920 phone_numbers = property(get_phone_numbers, set_phone_numbers)
921
922 @property
923 def _all_telephone_numbers(self):
924 ret = [ self.sip_id, ]
925
926 if self.phone_number:
927 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
928 ret.append(s)
929
930 for number in self.phone_numbers:
931 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
932 ret.append(s)
933
934 return ret
935
936 def avatar_url(self, size=None):
937 url = "https://people.ipfire.org/users/%s.jpg" % self.uid
938
939 if size:
940 url += "?size=%s" % size
941
942 return url
943
944 def get_avatar(self, size=None):
945 photo = self._get_bytes("jpegPhoto")
946
947 # Exit if no avatar is available
948 if not photo:
949 return
950
951 # Return the raw image if no size was requested
952 if size is None:
953 return photo
954
955 # Try to retrieve something from the cache
956 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
957 if avatar:
958 return avatar
959
960 # Generate a new thumbnail
961 avatar = util.generate_thumbnail(photo, size, square=True)
962
963 # Save to cache for 15m
964 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
965
966 return avatar
967
968 def upload_avatar(self, avatar):
969 self._set("jpegPhoto", avatar)
970
971 # SSH Keys
972
973 @lazy_property
974 def ssh_keys(self):
975 ret = []
976
977 for key in self._get_strings("sshPublicKey"):
978 s = sshpubkeys.SSHKey()
979
980 try:
981 s.parse(key)
982 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
983 logging.warning("Could not parse SSH key %s: %s" % (key, e))
984 continue
985
986 ret.append(s)
987
988 return ret
989
990 def get_ssh_key_by_hash_sha256(self, hash_sha256):
991 for key in self.ssh_keys:
992 if not key.hash_sha256() == hash_sha256:
993 continue
994
995 return key
996
997 def add_ssh_key(self, key):
998 k = sshpubkeys.SSHKey()
999
1000 # Try to parse the key
1001 k.parse(key)
1002
1003 # Check for types and sufficient sizes
1004 if k.key_type == b"ssh-rsa":
1005 if k.bits < 4096:
1006 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
1007
1008 elif k.key_type == b"ssh-dss":
1009 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
1010
1011 # Ignore any duplicates
1012 if key in (k.keydata for k in self.ssh_keys):
1013 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
1014 return
1015
1016 # Prepare transaction
1017 modlist = []
1018
1019 # Add object class if user is not in it, yet
1020 if not "ldapPublicKey" in self.classes:
1021 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
1022
1023 # Add key
1024 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
1025
1026 # Save key to LDAP
1027 self._modify(modlist)
1028
1029 # Append to cache
1030 self.ssh_keys.append(k)
1031
1032 def delete_ssh_key(self, key):
1033 if not key in (k.keydata for k in self.ssh_keys):
1034 return
1035
1036 # Delete key from LDAP
1037 if len(self.ssh_keys) > 1:
1038 self._delete_string("sshPublicKey", key)
1039 else:
1040 self._modify([
1041 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
1042 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
1043 ])
1044
1045
1046 class StopForumSpam(Object):
1047 def init(self, uid, email, address):
1048 self.uid, self.email, self.address = uid, email, address
1049
1050 @tornado.gen.coroutine
1051 def send_request(self, **kwargs):
1052 arguments = {
1053 "json" : "1",
1054 }
1055 arguments.update(kwargs)
1056
1057 # Create request
1058 request = tornado.httpclient.HTTPRequest(
1059 "https://api.stopforumspam.org/api", method="POST")
1060 request.body = urllib.parse.urlencode(arguments)
1061
1062 # Send the request
1063 response = yield self.backend.http_client.fetch(request)
1064
1065 # Decode the JSON response
1066 return json.loads(response.body.decode())
1067
1068 @tornado.gen.coroutine
1069 def check_address(self):
1070 response = yield self.send_request(ip=self.address)
1071
1072 try:
1073 confidence = response["ip"]["confidence"]
1074 except KeyError:
1075 confidence = 100
1076
1077 logging.debug("Confidence for %s: %s" % (self.address, confidence))
1078
1079 return confidence
1080
1081 @tornado.gen.coroutine
1082 def check_username(self):
1083 response = yield self.send_request(username=self.uid)
1084
1085 try:
1086 confidence = response["username"]["confidence"]
1087 except KeyError:
1088 confidence = 100
1089
1090 logging.debug("Confidence for %s: %s" % (self.uid, confidence))
1091
1092 return confidence
1093
1094 @tornado.gen.coroutine
1095 def check_email(self):
1096 response = yield self.send_request(email=self.email)
1097
1098 try:
1099 confidence = response["email"]["confidence"]
1100 except KeyError:
1101 confidence = 100
1102
1103 logging.debug("Confidence for %s: %s" % (self.email, confidence))
1104
1105 return confidence
1106
1107 @tornado.gen.coroutine
1108 def check(self, threshold=95):
1109 """
1110 This function tries to detect if we have a spammer.
1111
1112 To honour the privacy of our users, we only send the IP
1113 address and username and if those are on the database, we
1114 will send the email address as well.
1115 """
1116 confidences = yield [self.check_address(), self.check_username()]
1117
1118 if any((c < threshold for c in confidences)):
1119 confidences += yield [self.check_email()]
1120
1121 # Build a score based on the lowest confidence
1122 return 100 - min(confidences)
1123
1124
1125 if __name__ == "__main__":
1126 a = Accounts()
1127
1128 print(a.list())