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