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