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