]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
0d767ee474f18f0cc532e99b686155b0f16428e3
[ipfire.org.git] / src / backend / accounts.py
1 #!/usr/bin/python
2 # encoding: utf-8
3
4 import PIL
5 import PIL.ImageOps
6 import datetime
7 import io
8 import ldap
9 import ldap.modlist
10 import logging
11 import phonenumbers
12 import sshpubkeys
13 import urllib.parse
14 import urllib.request
15 import zxcvbn
16
17 from . import countries
18 from . import util
19 from .decorators import *
20 from .misc import Object
21
22 class Accounts(Object):
23 def __iter__(self):
24 # Only return developers (group with ID 1000)
25 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
26
27 return iter(sorted(accounts))
28
29 @lazy_property
30 def ldap(self):
31 # Connect to LDAP server
32 ldap_uri = self.settings.get("ldap_uri")
33 conn = ldap.initialize(ldap_uri)
34
35 # Bind with username and password
36 bind_dn = self.settings.get("ldap_bind_dn")
37 if bind_dn:
38 bind_pw = self.settings.get("ldap_bind_pw", "")
39 conn.simple_bind(bind_dn, bind_pw)
40
41 return conn
42
43 def _query(self, query, attrlist=None, limit=0):
44 logging.debug("Performing LDAP query: %s" % query)
45
46 search_base = self.settings.get("ldap_search_base")
47
48 try:
49 results = self.ldap.search_ext_s(search_base, ldap.SCOPE_SUBTREE,
50 query, attrlist=attrlist, sizelimit=limit)
51 except:
52 # Close current connection
53 del self.ldap
54
55 raise
56
57 return results
58
59 def _search(self, query, attrlist=None, limit=0):
60 accounts = []
61
62 for dn, attrs in self._query(query, attrlist=attrlist, limit=limit):
63 account = Account(self.backend, dn, attrs)
64 accounts.append(account)
65
66 return accounts
67
68 def search(self, query):
69 # Search for exact matches
70 accounts = self._search("(&(objectClass=person) \
71 (|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
72 % (query, query, query, query, query, query))
73
74 # Find accounts by name
75 if not accounts:
76 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
77 if not account in accounts:
78 accounts.append(account)
79
80 return sorted(accounts)
81
82 def _search_one(self, query):
83 result = self._search(query, limit=1)
84 assert len(result) <= 1
85
86 if result:
87 return result[0]
88
89 def uid_exists(self, uid):
90 if self.get_by_uid(uid):
91 return True
92
93 res = self.db.get("SELECT 1 FROM account_activations \
94 WHERE uid = %s AND expires_at > NOW()", uid)
95
96 if res:
97 return True
98
99 # Account with uid does not exist, yet
100 return False
101
102 def get_by_uid(self, uid):
103 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
104
105 def get_by_mail(self, mail):
106 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
107
108 find = get_by_uid
109
110 def find_account(self, s):
111 account = self.get_by_uid(s)
112 if account:
113 return account
114
115 return self.get_by_mail(s)
116
117 def get_by_sip_id(self, sip_id):
118 return self._search_one("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
119 (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id, sip_id))
120
121 def get_by_phone_number(self, number):
122 return self._search_one("(&(objectClass=inetOrgPerson) \
123 (|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
124 % (number, number, number, number))
125
126 # Registration
127
128 def create(self, uid, email, first_name, last_name):
129 # Check if UID is unique
130 if self.get_by_uid(uid):
131 raise ValueError("UID exists: %s" % uid)
132
133 activation_code = util.random_string(24)
134
135 # Account Parameters
136 account = {
137 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
138 "userPassword" : activation_code.encode(),
139 "mail" : email.encode(),
140
141 # Name
142 "cn" : b"%s %s" % (first_name.encode(), last_name.encode()),
143 "sn" : last_name.encode(),
144 "givenName" : first_name.encode(),
145 }
146
147 # Create account on LDAP
148 self.ldap.add_s("uid=%s,ou=People,dc=mcfly,dc=local" % uid, ldap.modlist.addModlist(account))
149
150 # TODO Send email with activation code
151 pass
152
153 # Session stuff
154
155 def _cleanup_expired_sessions(self):
156 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
157
158 def create_session(self, account, host):
159 self._cleanup_expired_sessions()
160
161 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
162 RETURNING session_id, time_expires", host, account.uid)
163
164 # Session could not be created
165 if not res:
166 return None, None
167
168 logging.info("Created session %s for %s which expires %s" \
169 % (res.session_id, account, res.time_expires))
170 return res.session_id, res.time_expires
171
172 def destroy_session(self, session_id, host):
173 logging.info("Destroying session %s" % session_id)
174
175 self.db.execute("DELETE FROM sessions \
176 WHERE session_id = %s AND host = %s", session_id, host)
177 self._cleanup_expired_sessions()
178
179 def get_by_session(self, session_id, host):
180 logging.debug("Looking up session %s" % session_id)
181
182 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
183 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
184 session_id, host)
185
186 # Session does not exist or has expired
187 if not res:
188 return
189
190 # Update the session expiration time
191 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
192 WHERE session_id = %s AND host = %s", session_id, host)
193
194 return self.get_by_uid(res.uid)
195
196
197 class Account(Object):
198 def __init__(self, backend, dn, attrs=None):
199 Object.__init__(self, backend)
200 self.dn = dn
201
202 self.attributes = attrs or {}
203
204 def __str__(self):
205 return self.name
206
207 def __repr__(self):
208 return "<%s %s>" % (self.__class__.__name__, self.dn)
209
210 def __eq__(self, other):
211 if isinstance(other, self.__class__):
212 return self.dn == other.dn
213
214 def __lt__(self, other):
215 if isinstance(other, self.__class__):
216 return self.name < other.name
217
218 @property
219 def ldap(self):
220 return self.accounts.ldap
221
222 def _exists(self, key):
223 try:
224 self.attributes[key]
225 except KeyError:
226 return False
227
228 return True
229
230 def _get(self, key):
231 for value in self.attributes.get(key, []):
232 yield value
233
234 def _get_bytes(self, key, default=None):
235 for value in self._get(key):
236 return value
237
238 return default
239
240 def _get_strings(self, key):
241 for value in self._get(key):
242 yield value.decode()
243
244 def _get_string(self, key, default=None):
245 for value in self._get_strings(key):
246 return value
247
248 return default
249
250 def _get_phone_numbers(self, key):
251 for value in self._get_strings(key):
252 yield phonenumbers.parse(value, None)
253
254 def _modify(self, modlist):
255 logging.debug("Modifying %s: %s" % (self.dn, modlist))
256
257 # Run modify operation
258 self.ldap.modify_s(self.dn, modlist)
259
260 def _set(self, key, values):
261 current = self._get(key)
262
263 # Don't do anything if nothing has changed
264 if list(current) == values:
265 return
266
267 # Remove all old values and add all new ones
268 modlist = []
269
270 if self._exists(key):
271 modlist.append((ldap.MOD_DELETE, key, None))
272
273 # Add new values
274 if values:
275 modlist.append((ldap.MOD_ADD, key, values))
276
277 # Run modify operation
278 self._modify(modlist)
279
280 # Update cache
281 self.attributes.update({ key : values })
282
283 def _set_bytes(self, key, values):
284 return self._set(key, values)
285
286 def _set_strings(self, key, values):
287 return self._set(key, [e.encode() for e in values if e])
288
289 def _set_string(self, key, value):
290 return self._set_strings(key, [value,])
291
292 def _add(self, key, values):
293 modlist = [
294 (ldap.MOD_ADD, key, values),
295 ]
296
297 self._modify(modlist)
298
299 def _add_strings(self, key, values):
300 return self._add(key, [e.encode() for e in values])
301
302 def _add_string(self, key, value):
303 return self._add_strings(key, [value,])
304
305 def _delete(self, key, values):
306 modlist = [
307 (ldap.MOD_DELETE, key, values),
308 ]
309
310 self._modify(modlist)
311
312 def _delete_strings(self, key, values):
313 return self._delete(key, [e.encode() for e in values])
314
315 def _delete_string(self, key, value):
316 return self._delete_strings(key, [value,])
317
318 def passwd(self, password):
319 """
320 Sets a new password
321 """
322 # The new password must have a score of 3 or better
323 quality = self.check_password_quality(password)
324 if quality["score"] < 3:
325 raise ValueError("Password too weak")
326
327 self.ldap.passwd_s(self.dn, None, password)
328
329 def check_password(self, password):
330 """
331 Bind to the server with given credentials and return
332 true if password is corrent and false if not.
333
334 Raises exceptions from the server on any other errors.
335 """
336 if not password:
337 return
338
339 logging.debug("Checking credentials for %s" % self.dn)
340
341 # Create a new LDAP connection
342 ldap_uri = self.backend.settings.get("ldap_uri")
343 conn = ldap.initialize(ldap_uri)
344
345 try:
346 conn.simple_bind_s(self.dn, password.encode("utf-8"))
347 except ldap.INVALID_CREDENTIALS:
348 logging.debug("Account credentials are invalid for %s" % self)
349 return False
350
351 logging.info("Successfully authenticated %s" % self)
352
353 return True
354
355 def check_password_quality(self, password):
356 """
357 Passwords are passed through zxcvbn to make sure
358 that they are strong enough.
359 """
360 return zxcvbn.zxcvbn(password, user_inputs=(
361 self.first_name, self.last_name,
362 ))
363
364 def is_admin(self):
365 return "wheel" in self.groups
366
367 def is_staff(self):
368 return "staff" in self.groups
369
370 def has_shell(self):
371 return "posixAccount" in self.classes
372
373 def has_mail(self):
374 return "postfixMailUser" in self.classes
375
376 def has_sip(self):
377 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
378
379 def can_be_managed_by(self, account):
380 """
381 Returns True if account is allowed to manage this account
382 """
383 # Admins can manage all accounts
384 if account.is_admin():
385 return True
386
387 # Users can manage themselves
388 return self == account
389
390 @property
391 def classes(self):
392 return self._get_strings("objectClass")
393
394 @property
395 def uid(self):
396 return self._get_string("uid")
397
398 @property
399 def name(self):
400 return self._get_string("cn")
401
402 # First Name
403
404 def get_first_name(self):
405 return self._get_string("givenName")
406
407 def set_first_name(self, first_name):
408 self._set_string("givenName", first_name)
409
410 # Update Common Name
411 self._set_string("cn", "%s %s" % (first_name, self.last_name))
412
413 first_name = property(get_first_name, set_first_name)
414
415 # Last Name
416
417 def get_last_name(self):
418 return self._get_string("sn")
419
420 def set_last_name(self, last_name):
421 self._set_string("sn", last_name)
422
423 # Update Common Name
424 self._set_string("cn", "%s %s" % (self.first_name, last_name))
425
426 last_name = property(get_last_name, set_last_name)
427
428 @lazy_property
429 def groups(self):
430 groups = []
431
432 res = self.accounts._query("(&(objectClass=posixGroup) \
433 (memberUid=%s))" % self.uid, ["cn"])
434
435 for dn, attrs in res:
436 cns = attrs.get("cn")
437 if cns:
438 groups.append(cns[0].decode())
439
440 return groups
441
442 # Address
443
444 @property
445 def address(self):
446 address = []
447
448 if self.street:
449 address += self.street.splitlines()
450
451 if self.postal_code and self.city:
452 if self.country_code in ("AT", "DE"):
453 address.append("%s %s" % (self.postal_code, self.city))
454 else:
455 address.append("%s, %s" % (self.city, self.postal_code))
456 else:
457 address.append(self.city or self.postal_code)
458
459 if self.country_name:
460 address.append(self.country_name)
461
462 return address
463
464 def get_street(self):
465 return self._get_string("street") or self._get_string("homePostalAddress")
466
467 def set_street(self, street):
468 self._set_string("street", street)
469
470 street = property(get_street, set_street)
471
472 def get_city(self):
473 return self._get_string("l") or ""
474
475 def set_city(self, city):
476 self._set_string("l", city)
477
478 city = property(get_city, set_city)
479
480 def get_postal_code(self):
481 return self._get_string("postalCode") or ""
482
483 def set_postal_code(self, postal_code):
484 self._set_string("postalCode", postal_code)
485
486 postal_code = property(get_postal_code, set_postal_code)
487
488 # XXX This should be c
489 def get_country_code(self):
490 return self._get_string("st")
491
492 def set_country_code(self, country_code):
493 self._set_string("st", country_code)
494
495 country_code = property(get_country_code, set_country_code)
496
497 @property
498 def country_name(self):
499 if self.country_code:
500 return countries.get_name(self.country_code)
501
502 @property
503 def email(self):
504 name = self.name.lower()
505 name = name.replace(" ", ".")
506 name = name.replace("Ä", "Ae")
507 name = name.replace("Ö", "Oe")
508 name = name.replace("Ü", "Ue")
509 name = name.replace("ä", "ae")
510 name = name.replace("ö", "oe")
511 name = name.replace("ü", "ue")
512
513 for mail in self.attributes.get("mail", []):
514 if mail.decode().startswith("%s@ipfire.org" % name):
515 return mail
516
517 # If everything else fails, we will go with the UID
518 return "%s@ipfire.org" % self.uid
519
520 # Mail Routing Address
521
522 def get_mail_routing_address(self):
523 return self._get_string("mailRoutingAddress", None)
524
525 def set_mail_routing_address(self, address):
526 self._set_string("mailRoutingAddress", address or None)
527
528 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
529
530 @property
531 def sip_id(self):
532 if "sipUser" in self.classes:
533 return self._get_string("sipAuthenticationUser")
534
535 if "sipRoutingObject" in self.classes:
536 return self._get_string("sipLocalAddress")
537
538 @property
539 def sip_password(self):
540 return self._get_string("sipPassword")
541
542 @staticmethod
543 def _generate_sip_password():
544 return util.random_string(8)
545
546 @property
547 def sip_url(self):
548 return "%s@ipfire.org" % self.sip_id
549
550 def uses_sip_forwarding(self):
551 if self.sip_routing_address:
552 return True
553
554 return False
555
556 # SIP Routing
557
558 def get_sip_routing_address(self):
559 if "sipRoutingObject" in self.classes:
560 return self._get_string("sipRoutingAddress")
561
562 def set_sip_routing_address(self, address):
563 if not address:
564 address = None
565
566 # Don't do anything if nothing has changed
567 if self.get_sip_routing_address() == address:
568 return
569
570 if address:
571 modlist = [
572 # This is no longer a SIP user any more
573 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
574 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
575 (ldap.MOD_DELETE, "sipPassword", None),
576
577 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
578 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
579 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
580 ]
581 else:
582 modlist = [
583 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
584 (ldap.MOD_DELETE, "sipLocalAddress", None),
585 (ldap.MOD_DELETE, "sipRoutingAddress", None),
586
587 (ldap.MOD_ADD, "objectClass", b"sipUser"),
588 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
589 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
590 ]
591
592 # Run modification
593 self._modify(modlist)
594
595 # XXX Cache is invalid here
596
597 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
598
599 @lazy_property
600 def sip_registrations(self):
601 sip_registrations = []
602
603 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
604 reg.account = self
605
606 sip_registrations.append(reg)
607
608 return sip_registrations
609
610 @lazy_property
611 def sip_channels(self):
612 return self.backend.talk.freeswitch.get_sip_channels(self)
613
614 def get_cdr(self, date=None, limit=None):
615 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
616
617 # Phone Numbers
618
619 @lazy_property
620 def phone_number(self):
621 """
622 Returns the IPFire phone number
623 """
624 if self.sip_id:
625 return phonenumbers.parse("+4923636035%s" % self.sip_id)
626
627 @lazy_property
628 def fax_number(self):
629 if self.sip_id:
630 return phonenumbers.parse("+49236360359%s" % self.sip_id)
631
632 def get_phone_numbers(self):
633 ret = []
634
635 for field in ("telephoneNumber", "homePhone", "mobile"):
636 for number in self._get_phone_numbers(field):
637 ret.append(number)
638
639 return ret
640
641 def set_phone_numbers(self, phone_numbers):
642 # Sort phone numbers by landline and mobile
643 _landline_numbers = []
644 _mobile_numbers = []
645
646 for number in phone_numbers:
647 try:
648 number = phonenumbers.parse(number, None)
649 except phonenumbers.phonenumberutil.NumberParseException:
650 continue
651
652 # Convert to string (in E.164 format)
653 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
654
655 # Separate mobile numbers
656 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
657 _mobile_numbers.append(s)
658 else:
659 _landline_numbers.append(s)
660
661 # Save
662 self._set_strings("telephoneNumber", _landline_numbers)
663 self._set_strings("mobile", _mobile_numbers)
664
665 phone_numbers = property(get_phone_numbers, set_phone_numbers)
666
667 @property
668 def _all_telephone_numbers(self):
669 ret = [ self.sip_id, ]
670
671 if self.phone_number:
672 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
673 ret.append(s)
674
675 for number in self.phone_numbers:
676 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
677 ret.append(s)
678
679 return ret
680
681 def avatar_url(self, size=None):
682 if self.backend.debug:
683 hostname = "http://people.dev.ipfire.org"
684 else:
685 hostname = "https://people.ipfire.org"
686
687 url = "%s/users/%s.jpg" % (hostname, self.uid)
688
689 if size:
690 url += "?size=%s" % size
691
692 return url
693
694 def get_avatar(self, size=None):
695 avatar = self._get_bytes("jpegPhoto")
696 if not avatar:
697 return
698
699 if not size:
700 return avatar
701
702 return self._resize_avatar(avatar, size)
703
704 def _resize_avatar(self, image, size):
705 image = PIL.Image.open(io.BytesIO(image))
706
707 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
708 if image.mode == "RGBA":
709 image = image.convert("RGB")
710
711 # Resize the image to the desired resolution (and make it square)
712 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
713
714 with io.BytesIO() as f:
715 # If writing out the image does not work with optimization,
716 # we try to write it out without any optimization.
717 try:
718 thumbnail.save(f, "JPEG", optimize=True, quality=98)
719 except:
720 thumbnail.save(f, "JPEG", quality=98)
721
722 return f.getvalue()
723
724 def upload_avatar(self, avatar):
725 self._set("jpegPhoto", avatar)
726
727 # SSH Keys
728
729 @lazy_property
730 def ssh_keys(self):
731 ret = []
732
733 for key in self._get_strings("sshPublicKey"):
734 s = sshpubkeys.SSHKey()
735
736 try:
737 s.parse(key)
738 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
739 logging.warning("Could not parse SSH key %s: %s" % (key, e))
740 continue
741
742 ret.append(s)
743
744 return ret
745
746 def get_ssh_key_by_hash_sha256(self, hash_sha256):
747 for key in self.ssh_keys:
748 if not key.hash_sha256() == hash_sha256:
749 continue
750
751 return key
752
753 def add_ssh_key(self, key):
754 k = sshpubkeys.SSHKey()
755
756 # Try to parse the key
757 k.parse(key)
758
759 # Check for types and sufficient sizes
760 if k.key_type == b"ssh-rsa":
761 if k.bits < 4096:
762 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
763
764 elif k.key_type == b"ssh-dss":
765 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
766
767 # Ignore any duplicates
768 if key in (k.keydata for k in self.ssh_keys):
769 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
770 return
771
772 # Prepare transaction
773 modlist = []
774
775 # Add object class if user is not in it, yet
776 if not "ldapPublicKey" in self.classes:
777 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
778
779 # Add key
780 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
781
782 # Save key to LDAP
783 self._modify(modlist)
784
785 # Append to cache
786 self.ssh_keys.append(k)
787
788 def delete_ssh_key(self, key):
789 if not key in (k.keydata for k in self.ssh_keys):
790 return
791
792 # Delete key from LDAP
793 if len(self.ssh_keys) > 1:
794 self._delete_string("sshPublicKey", key)
795 else:
796 self._modify([
797 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
798 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
799 ])
800
801
802 if __name__ == "__main__":
803 a = Accounts()
804
805 print(a.list())