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