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