]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
c2f19e6449d8661ee1f8019495b28d166fadaea1
[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 "sipUser" in self.classes or "sipRoutingObject" in self.classes
833
834 def is_lwl(self):
835 return self.is_member_of_group("lwl-staff")
836
837 def can_be_managed_by(self, account):
838 """
839 Returns True if account is allowed to manage this account
840 """
841 # Admins can manage all accounts
842 if account.is_admin():
843 return True
844
845 # Users can manage themselves
846 return self == account
847
848 @property
849 def classes(self):
850 return self._get_strings("objectClass")
851
852 @property
853 def uid(self):
854 return self._get_string("uid")
855
856 @property
857 def name(self):
858 return self._get_string("cn")
859
860 # Delete
861
862 async def delete(self, user):
863 """
864 Deletes this user
865 """
866 # Check if this user can be deleted
867 if not self.can_be_deleted_by(user):
868 raise RuntimeError("Cannot delete user %s" % self)
869
870 logging.info("Deleting user %s" % self)
871
872 async with asyncio.TaskGroup() as tasks:
873 t = datetime.datetime.now()
874
875 # Disable this account on Bugzilla
876 tasks.create_task(
877 self._disable_on_bugzilla("Deleted by %s, %s" % (user, t)),
878 )
879
880 # XXX Delete on Discourse
881
882 # Delete on LDAP
883 self._delete()
884
885 def can_be_deleted_by(self, user):
886 """
887 Return True if the user can be deleted by user
888 """
889 # Check permissions
890 if not self.can_be_managed_by(user):
891 return False
892
893 # Cannot delete shell users
894 if self.has_shell():
895 return False
896
897 # Looks okay
898 return True
899
900 def _delete(self):
901 """
902 Deletes this object from LDAP
903 """
904 # Delete the Kerberos Principal
905 self._delete_dn(self.kerberos_principal_dn)
906
907 # Delete this object
908 self._delete_dn(self.dn)
909
910 # Nickname
911
912 def get_nickname(self):
913 return self._get_string("displayName")
914
915 def set_nickname(self, nickname):
916 self._set_string("displayName", nickname)
917
918 nickname = property(get_nickname, set_nickname)
919
920 # First Name
921
922 def get_first_name(self):
923 return self._get_string("givenName")
924
925 def set_first_name(self, first_name):
926 self._set_string("givenName", first_name)
927
928 # Update Common Name
929 self._set_string("cn", "%s %s" % (first_name, self.last_name))
930
931 first_name = property(get_first_name, set_first_name)
932
933 # Last Name
934
935 def get_last_name(self):
936 return self._get_string("sn")
937
938 def set_last_name(self, last_name):
939 self._set_string("sn", last_name)
940
941 # Update Common Name
942 self._set_string("cn", "%s %s" % (self.first_name, last_name))
943
944 last_name = property(get_last_name, set_last_name)
945
946 @lazy_property
947 def groups(self):
948 return self.backend.groups._get_groups("(| \
949 (&(objectClass=groupOfNames)(member=%s)) \
950 (&(objectClass=posixGroup)(memberUid=%s)) \
951 )" % (self.dn, self.uid))
952
953 def is_member_of_group(self, gid):
954 """
955 Returns True if this account is a member of this group
956 """
957 return gid in (g.gid for g in self.groups)
958
959 # Created/Modified at
960
961 @property
962 def created_at(self):
963 return self._get_timestamp("createTimestamp")
964
965 @property
966 def modified_at(self):
967 return self._get_timestamp("modifyTimestamp")
968
969 # Address
970
971 @property
972 def address(self):
973 address = []
974
975 if self.street:
976 address += self.street.splitlines()
977
978 if self.postal_code and self.city:
979 if self.country_code in ("AT", "DE"):
980 address.append("%s %s" % (self.postal_code, self.city))
981 else:
982 address.append("%s, %s" % (self.city, self.postal_code))
983 else:
984 address.append(self.city or self.postal_code)
985
986 if self.country_name:
987 address.append(self.country_name)
988
989 return [line for line in address if line]
990
991 def get_street(self):
992 return self._get_string("street") or self._get_string("homePostalAddress")
993
994 def set_street(self, street):
995 self._set_string("street", street)
996
997 street = property(get_street, set_street)
998
999 def get_city(self):
1000 return self._get_string("l") or ""
1001
1002 def set_city(self, city):
1003 self._set_string("l", city)
1004
1005 city = property(get_city, set_city)
1006
1007 def get_postal_code(self):
1008 return self._get_string("postalCode") or ""
1009
1010 def set_postal_code(self, postal_code):
1011 self._set_string("postalCode", postal_code)
1012
1013 postal_code = property(get_postal_code, set_postal_code)
1014
1015 # XXX This should be c
1016 def get_country_code(self):
1017 return self._get_string("st")
1018
1019 def set_country_code(self, country_code):
1020 self._set_string("st", country_code)
1021
1022 country_code = property(get_country_code, set_country_code)
1023
1024 @property
1025 def country_name(self):
1026 if self.country_code:
1027 return self.backend.get_country_name(self.country_code)
1028
1029 @property
1030 def email(self):
1031 return self._get_string("mail")
1032
1033 @property
1034 def email_to(self):
1035 return "%s <%s>" % (self, self.email)
1036
1037 @lazy_property
1038 def alternate_email_addresses(self):
1039 addresses = self._get_strings("mailAlternateAddress")
1040
1041 return sorted(addresses)
1042
1043 # Mail Routing Address
1044
1045 def get_mail_routing_address(self):
1046 return self._get_string("mailRoutingAddress", None)
1047
1048 def set_mail_routing_address(self, address):
1049 self._set_string("mailRoutingAddress", address or None)
1050
1051 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
1052
1053 @property
1054 def sip_id(self):
1055 if "sipUser" in self.classes:
1056 return self._get_string("sipAuthenticationUser")
1057
1058 if "sipRoutingObject" in self.classes:
1059 return self._get_string("sipLocalAddress")
1060
1061 @property
1062 def sip_password(self):
1063 return self._get_string("sipPassword")
1064
1065 @staticmethod
1066 def _generate_sip_password():
1067 return util.random_string(8)
1068
1069 @property
1070 def sip_url(self):
1071 return "%s@ipfire.org" % self.sip_id
1072
1073 @lazy_property
1074 def agent_status(self):
1075 return self.backend.talk.freeswitch.get_agent_status(self)
1076
1077 def uses_sip_forwarding(self):
1078 if self.sip_routing_address:
1079 return True
1080
1081 return False
1082
1083 # SIP Routing
1084
1085 def get_sip_routing_address(self):
1086 if "sipRoutingObject" in self.classes:
1087 return self._get_string("sipRoutingAddress")
1088
1089 def set_sip_routing_address(self, address):
1090 if not address:
1091 address = None
1092
1093 # Don't do anything if nothing has changed
1094 if self.get_sip_routing_address() == address:
1095 return
1096
1097 if address:
1098 # This is no longer a SIP user any more
1099 try:
1100 self._modify([
1101 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
1102 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
1103 (ldap.MOD_DELETE, "sipPassword", None),
1104 ])
1105 except ldap.NO_SUCH_ATTRIBUTE:
1106 pass
1107
1108 # Set new routing object
1109 try:
1110 self._modify([
1111 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
1112 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
1113 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
1114 ])
1115
1116 # If this is a change, we cannot add this again
1117 except ldap.TYPE_OR_VALUE_EXISTS:
1118 self._set_string("sipRoutingAddress", address)
1119 else:
1120 try:
1121 self._modify([
1122 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
1123 (ldap.MOD_DELETE, "sipLocalAddress", None),
1124 (ldap.MOD_DELETE, "sipRoutingAddress", None),
1125 ])
1126 except ldap.NO_SUCH_ATTRIBUTE:
1127 pass
1128
1129 self._modify([
1130 (ldap.MOD_ADD, "objectClass", b"sipUser"),
1131 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
1132 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
1133 ])
1134
1135 # XXX Cache is invalid here
1136
1137 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
1138
1139 # SIP Registrations
1140
1141 async def get_sip_registrations(self):
1142 if not self.has_sip():
1143 return []
1144
1145 return await self.backend.asterisk.get_registrations(self.sip_id)
1146
1147 # SIP Channels
1148
1149 async def get_sip_channels(self):
1150 if not self.has_sip():
1151 return []
1152
1153 return await self.backend.asterisk.get_sip_channels(self.sip_id)
1154
1155 # Phone Numbers
1156
1157 @lazy_property
1158 def phone_number(self):
1159 """
1160 Returns the IPFire phone number
1161 """
1162 if self.sip_id:
1163 return phonenumbers.parse("+4923636035%s" % self.sip_id)
1164
1165 @lazy_property
1166 def fax_number(self):
1167 if self.sip_id:
1168 return phonenumbers.parse("+49236360359%s" % self.sip_id)
1169
1170 def get_phone_numbers(self):
1171 ret = []
1172
1173 for field in ("telephoneNumber", "homePhone", "mobile"):
1174 for number in self._get_phone_numbers(field):
1175 ret.append(number)
1176
1177 return ret
1178
1179 def set_phone_numbers(self, phone_numbers):
1180 # Sort phone numbers by landline and mobile
1181 _landline_numbers = []
1182 _mobile_numbers = []
1183
1184 for number in phone_numbers:
1185 try:
1186 number = phonenumbers.parse(number, None)
1187 except phonenumbers.phonenumberutil.NumberParseException:
1188 continue
1189
1190 # Convert to string (in E.164 format)
1191 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
1192
1193 # Separate mobile numbers
1194 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
1195 _mobile_numbers.append(s)
1196 else:
1197 _landline_numbers.append(s)
1198
1199 # Save
1200 self._set_strings("telephoneNumber", _landline_numbers)
1201 self._set_strings("mobile", _mobile_numbers)
1202
1203 phone_numbers = property(get_phone_numbers, set_phone_numbers)
1204
1205 @property
1206 def _all_telephone_numbers(self):
1207 ret = [ self.sip_id, ]
1208
1209 if self.phone_number:
1210 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
1211 ret.append(s)
1212
1213 for number in self.phone_numbers:
1214 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
1215 ret.append(s)
1216
1217 return ret
1218
1219 # Description
1220
1221 def get_description(self):
1222 return self._get_string("description")
1223
1224 def set_description(self, description):
1225 self._set_string("description", description)
1226
1227 description = property(get_description, set_description)
1228
1229 # Avatar
1230
1231 def has_avatar(self):
1232 has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
1233 if has_avatar is None:
1234 has_avatar = True if self.get_avatar() else False
1235
1236 # Cache avatar status for up to 24 hours
1237 self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
1238
1239 return has_avatar
1240
1241 def avatar_url(self, size=None, absolute=False):
1242 url = "/users/%s.jpg?h=%s" % (self.uid, self.avatar_hash)
1243
1244 # Return an absolute URL
1245 if absolute:
1246 url = urllib.parse.urljoin("https://people.ipfire.org", url)
1247
1248 if size:
1249 url += "&size=%s" % size
1250
1251 return url
1252
1253 def get_avatar(self, size=None):
1254 photo = self._get_bytes("jpegPhoto")
1255
1256 # Exit if no avatar is available
1257 if not photo:
1258 return
1259
1260 # Return the raw image if no size was requested
1261 if size is None:
1262 return photo
1263
1264 # Try to retrieve something from the cache
1265 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
1266 if avatar:
1267 return avatar
1268
1269 # Generate a new thumbnail
1270 avatar = util.generate_thumbnail(photo, size, square=True)
1271
1272 # Save to cache for 15m
1273 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
1274
1275 return avatar
1276
1277 @property
1278 def avatar_hash(self):
1279 hash = self.memcache.get("accounts:%s:avatar-hash" % self.dn)
1280 if not hash:
1281 h = hashlib.new("md5")
1282 h.update(self.get_avatar() or b"")
1283 hash = h.hexdigest()[:7]
1284
1285 self.memcache.set("accounts:%s:avatar-hash" % self.dn, hash, 86400)
1286
1287 return hash
1288
1289 def upload_avatar(self, avatar):
1290 self._set("jpegPhoto", avatar)
1291
1292 # Delete cached avatar status
1293 self.memcache.delete("accounts:%s:has-avatar" % self.dn)
1294
1295 # Delete avatar hash
1296 self.memcache.delete("accounts:%s:avatar-hash" % self.dn)
1297
1298 # Consent to promotional emails
1299
1300 def get_consents_to_promotional_emails(self):
1301 return self.is_member_of_group("promotional-consent")
1302
1303 def set_contents_to_promotional_emails(self, value):
1304 group = self.backend.groups.get_by_gid("promotional-consent")
1305 assert group, "Could not find group: promotional-consent"
1306
1307 if value is True:
1308 group.add_member(self)
1309 else:
1310 group.del_member(self)
1311
1312 consents_to_promotional_emails = property(
1313 get_consents_to_promotional_emails,
1314 set_contents_to_promotional_emails,
1315 )
1316
1317 # Bugzilla
1318
1319 async def _disable_on_bugzilla(self, text=None):
1320 """
1321 Disables the user on Bugzilla
1322 """
1323 user = await self.backend.bugzilla.get_user(self.email)
1324
1325 # Do nothing if the user does not exist
1326 if not user:
1327 return
1328
1329 # Disable the user
1330 await user.disable(text)
1331
1332
1333 class Groups(Object):
1334 hidden_groups = (
1335 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1336 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
1337
1338 # Everyone is a member of people
1339 "cn=people,ou=Group,dc=ipfire,dc=org",
1340 )
1341
1342 @property
1343 def search_base(self):
1344 return "ou=Group,%s" % self.backend.accounts.search_base
1345
1346 def _query(self, *args, **kwargs):
1347 kwargs.update({
1348 "search_base" : self.backend.groups.search_base,
1349 })
1350
1351 return self.backend.accounts._query(*args, **kwargs)
1352
1353 def __iter__(self):
1354 groups = self.get_all()
1355
1356 return iter(groups)
1357
1358 def _get_groups(self, query, **kwargs):
1359 res = self._query(query, **kwargs)
1360
1361 groups = []
1362 for dn, attrs in res:
1363 # Skip any hidden groups
1364 if dn in self.hidden_groups:
1365 continue
1366
1367 g = Group(self.backend, dn, attrs)
1368 groups.append(g)
1369
1370 return sorted(groups)
1371
1372 def _get_group(self, query, **kwargs):
1373 kwargs.update({
1374 "limit" : 1,
1375 })
1376
1377 groups = self._get_groups(query, **kwargs)
1378 if groups:
1379 return groups[0]
1380
1381 def get_all(self):
1382 return self._get_groups(
1383 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1384 )
1385
1386 def get_by_gid(self, gid):
1387 return self._get_group(
1388 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid,
1389 )
1390
1391
1392 class Group(LDAPObject):
1393 def __repr__(self):
1394 if self.description:
1395 return "<%s %s (%s)>" % (
1396 self.__class__.__name__,
1397 self.gid,
1398 self.description,
1399 )
1400
1401 return "<%s %s>" % (self.__class__.__name__, self.gid)
1402
1403 def __str__(self):
1404 return self.description or self.gid
1405
1406 def __lt__(self, other):
1407 if isinstance(other, self.__class__):
1408 return (self.description or self.gid) < (other.description or other.gid)
1409
1410 return NotImplemented
1411
1412 def __bool__(self):
1413 return True
1414
1415 def __len__(self):
1416 """
1417 Returns the number of members in this group
1418 """
1419 l = 0
1420
1421 for attr in ("member", "memberUid"):
1422 a = self.attributes.get(attr, None)
1423 if a:
1424 l += len(a)
1425
1426 return l
1427
1428 def __iter__(self):
1429 return iter(self.members)
1430
1431 @property
1432 def gid(self):
1433 return self._get_string("cn")
1434
1435 @property
1436 def description(self):
1437 return self._get_string("description")
1438
1439 @property
1440 def email(self):
1441 return self._get_string("mail")
1442
1443 @lazy_property
1444 def members(self):
1445 members = []
1446
1447 # Get all members by DN
1448 for dn in self._get_strings("member"):
1449 member = self.backend.accounts.get_by_dn(dn)
1450 if member:
1451 members.append(member)
1452
1453 # Get all members by UID
1454 for uid in self._get_strings("memberUid"):
1455 member = self.backend.accounts.get_by_uid(uid)
1456 if member:
1457 members.append(member)
1458
1459 return sorted(members)
1460
1461 def add_member(self, account):
1462 """
1463 Adds a member to this group
1464 """
1465 # Do nothing if this user is already in the group
1466 if account.is_member_of_group(self.gid):
1467 return
1468
1469 if "posixGroup" in self.objectclasses:
1470 self._add_string("memberUid", account.uid)
1471 else:
1472 self._add_string("member", account.dn)
1473
1474 # Append to cached list of members
1475 self.members.append(account)
1476 self.members.sort()
1477
1478 def del_member(self, account):
1479 """
1480 Removes a member from a group
1481 """
1482 # Do nothing if this user is not in the group
1483 if not account.is_member_of_group(self.gid):
1484 return
1485
1486 if "posixGroup" in self.objectclasses:
1487 self._delete_string("memberUid", account.uid)
1488 else:
1489 self._delete_string("member", account.dn)
1490
1491
1492 if __name__ == "__main__":
1493 a = Accounts()
1494
1495 print(a.list())