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