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