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