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