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