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