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