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