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