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