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