]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
location: Show blacklist status of IP addresses
[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
940227cb 15
e96e445b 16from . import util
917434b8 17from .decorators import *
11347e46 18from .misc import Object
940227cb 19
a6dc0bad 20class Accounts(Object):
9f05796c
MT
21 def __iter__(self):
22 # Only return developers (group with ID 1000)
1bae74c7 23 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
9f05796c 24
1bae74c7 25 return iter(sorted(accounts))
9f05796c 26
0ab42c1d 27 @lazy_property
66862195 28 def ldap(self):
0ab42c1d
MT
29 # Connect to LDAP server
30 ldap_uri = self.settings.get("ldap_uri")
31 conn = ldap.initialize(ldap_uri)
940227cb 32
0ab42c1d
MT
33 # Bind with username and password
34 bind_dn = self.settings.get("ldap_bind_dn")
35 if bind_dn:
36 bind_pw = self.settings.get("ldap_bind_pw", "")
37 conn.simple_bind(bind_dn, bind_pw)
66862195 38
0ab42c1d 39 return conn
940227cb 40
1bae74c7 41 def _query(self, query, attrlist=None, limit=0):
66862195 42 logging.debug("Performing LDAP query: %s" % query)
940227cb 43
66862195 44 search_base = self.settings.get("ldap_search_base")
a395634c 45
a69e87a1
MT
46 try:
47 results = self.ldap.search_ext_s(search_base, ldap.SCOPE_SUBTREE,
48 query, attrlist=attrlist, sizelimit=limit)
49 except:
50 # Close current connection
0ab42c1d
MT
51 self.ldap.close()
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
MT
232 # Add new values
233 modlist.append((ldap.MOD_ADD, key, values))
234
235 # Run modify operation
236 self._modify(modlist)
237
238 # Update cache
239 self.attributes.update({ key : values })
240
241 def _set_bytes(self, key, values):
242 return self._set(key, values)
243
244 def _set_strings(self, key, values):
245 return self._set(key, [e.encode() for e in values])
246
247 def _set_string(self, key, value):
248 return self._set_strings(key, [value,])
940227cb 249
0d1fb712
MT
250 def _add(self, key, values):
251 modlist = [
252 (ldap.MOD_ADD, key, values),
253 ]
254
255 self._modify(modlist)
256
257 def _add_strings(self, key, values):
258 return self._add(key, [e.encode() for e in values])
259
260 def _add_string(self, key, value):
261 return self._add_strings(key, [value,])
262
263 def _delete(self, key, values):
264 modlist = [
265 (ldap.MOD_DELETE, key, values),
266 ]
267
268 self._modify(modlist)
269
270 def _delete_strings(self, key, values):
271 return self._delete(key, [e.encode() for e in values])
272
273 def _delete_string(self, key, value):
274 return self._delete_strings(key, [value,])
275
3ea97943
MT
276 def passwd(self, new_password):
277 """
278 Sets a new password
279 """
280 self.ldap.passwd_s(self.dn, None, new_password)
281
940227cb
MT
282 def check_password(self, password):
283 """
284 Bind to the server with given credentials and return
285 true if password is corrent and false if not.
286
287 Raises exceptions from the server on any other errors.
288 """
0d1fb712
MT
289 if not password:
290 return
291
940227cb 292 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
293
294 # Create a new LDAP connection
295 ldap_uri = self.backend.settings.get("ldap_uri")
296 conn = ldap.initialize(ldap_uri)
297
940227cb 298 try:
3ea97943 299 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 300 except ldap.INVALID_CREDENTIALS:
3ea97943 301 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
302 return False
303
3ea97943
MT
304 logging.info("Successfully authenticated %s" % self)
305
940227cb
MT
306 return True
307
940227cb 308 def is_admin(self):
d82bc8e3 309 return "wheel" in self.groups
66862195
MT
310
311 def is_talk_enabled(self):
06c1d39c
MT
312 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
313 or self.telephone_numbers or self.address
66862195 314
e96e445b
MT
315 def can_be_managed_by(self, account):
316 """
317 Returns True if account is allowed to manage this account
318 """
319 # Admins can manage all accounts
320 if account.is_admin():
321 return True
322
323 # Users can manage themselves
324 return self == account
325
66862195
MT
326 @property
327 def classes(self):
e96e445b 328 return self._get_strings("objectClass")
66862195
MT
329
330 @property
331 def uid(self):
e96e445b 332 return self._get_string("uid")
940227cb 333
a6dc0bad
MT
334 @property
335 def name(self):
e96e445b 336 return self._get_string("cn")
66862195 337
e96e445b
MT
338 # First Name
339
340 def get_first_name(self):
341 return self._get_string("givenName")
342
343 def set_first_name(self, first_name):
344 self._set_string("givenName", first_name)
345
346 # Update Common Name
347 self._set_string("cn", "%s %s" % (first_name, self.last_name))
348
349 first_name = property(get_first_name, set_first_name)
350
351 # Last Name
352
353 def get_last_name(self):
354 return self._get_string("sn")
355
356 def set_last_name(self, last_name):
357 self._set_string("sn", last_name)
358
359 # Update Common Name
360 self._set_string("cn", "%s %s" % (self.first_name, last_name))
361
362 last_name = property(get_last_name, set_last_name)
66862195 363
1bae74c7 364 @lazy_property
66862195 365 def groups(self):
1bae74c7 366 groups = []
66862195 367
1bae74c7
MT
368 res = self.accounts._query("(&(objectClass=posixGroup) \
369 (memberUid=%s))" % self.uid, ["cn"])
66862195 370
1bae74c7
MT
371 for dn, attrs in res:
372 cns = attrs.get("cn")
373 if cns:
374 groups.append(cns[0].decode())
66862195 375
1bae74c7 376 return groups
66862195 377
e96e445b
MT
378 # Address
379
380 def get_address(self):
381 address = self._get_string("homePostalAddress")
382
383 if address:
384 return (line.strip() for line in address.split(","))
66862195 385
e96e445b
MT
386 return []
387
388 def set_address(self, address):
389 data = ", ".join(address.splitlines())
390
391 self._set_bytes("homePostalAddress", data.encode())
392
393 address = property(get_address, set_address)
a6dc0bad 394
940227cb
MT
395 @property
396 def email(self):
66862195 397 name = self.name.lower()
940227cb 398 name = name.replace(" ", ".")
78fdedae
MT
399 name = name.replace("Ä", "Ae")
400 name = name.replace("Ö", "Oe")
401 name = name.replace("Ü", "Ue")
402 name = name.replace("ä", "ae")
403 name = name.replace("ö", "oe")
404 name = name.replace("ü", "ue")
940227cb 405
66862195 406 for mail in self.attributes.get("mail", []):
7aee4b8d 407 if mail.decode().startswith("%s@ipfire.org" % name):
940227cb
MT
408 return mail
409
2cd9af74
MT
410 # If everything else fails, we will go with the UID
411 return "%s@ipfire.org" % self.uid
940227cb 412
e96e445b
MT
413 # Mail Routing Address
414
415 def get_mail_routing_address(self):
416 return self._get_string("mailRoutingAddress", None)
417
418 def set_mail_routing_address(self, address):
419 self._set_string("mailRoutingAddress", address)
420
421 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
422
66862195
MT
423 @property
424 def sip_id(self):
425 if "sipUser" in self.classes:
e96e445b 426 return self._get_string("sipAuthenticationUser")
66862195
MT
427
428 if "sipRoutingObject" in self.classes:
e96e445b 429 return self._get_string("sipLocalAddress")
66862195 430
2f51147a
MT
431 @property
432 def sip_password(self):
e96e445b
MT
433 return self._get_string("sipPassword")
434
435 @staticmethod
436 def _generate_sip_password():
437 return util.random_string(8)
2f51147a 438
66862195
MT
439 @property
440 def sip_url(self):
441 return "%s@ipfire.org" % self.sip_id
442
443 def uses_sip_forwarding(self):
e96e445b 444 if self.sip_routing_address:
66862195
MT
445 return True
446
447 return False
448
e96e445b
MT
449 # SIP Routing
450
451 def get_sip_routing_address(self):
66862195 452 if "sipRoutingObject" in self.classes:
e96e445b
MT
453 return self._get_string("sipRoutingAddress")
454
455 def set_sip_routing_address(self, address):
456 if not address:
457 address = None
458
459 # Don't do anything if nothing has changed
460 if self.get_sip_routing_address() == address:
461 return
462
463 if address:
464 modlist = [
465 # This is no longer a SIP user any more
466 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
467 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
468 (ldap.MOD_DELETE, "sipPassword", None),
469
470 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
471 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
472 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
473 ]
474 else:
475 modlist = [
476 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
477 (ldap.MOD_DELETE, "sipLocalAddress", None),
478 (ldap.MOD_DELETE, "sipRoutingAddress", None),
479
480 (ldap.MOD_ADD, "objectClass", b"sipUser"),
481 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
482 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
483 ]
484
485 # Run modification
486 self._modify(modlist)
487
488 # XXX Cache is invalid here
489
490 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 491
917434b8
MT
492 @lazy_property
493 def sip_registrations(self):
494 sip_registrations = []
495
496 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
497 reg.account = self
498
499 sip_registrations.append(reg)
500
501 return sip_registrations
502
1f38be5a
MT
503 @lazy_property
504 def sip_channels(self):
505 return self.backend.talk.freeswitch.get_sip_channels(self)
506
bdaf6b46
MT
507 def get_cdr(self, date=None, limit=None):
508 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 509
e96e445b 510 # Phone Numbers
6ff61434 511
d3208ac7
MT
512 @lazy_property
513 def phone_number(self):
514 """
515 Returns the IPFire phone number
516 """
517 if self.sip_id:
518 return phonenumbers.parse("+4923636035%s" % self.sip_id)
519
520 @lazy_property
521 def fax_number(self):
522 if self.sip_id:
523 return phonenumbers.parse("+49236360359%s" % self.sip_id)
524
e96e445b
MT
525 def get_phone_numbers(self):
526 ret = []
6ff61434 527
e96e445b
MT
528 for field in ("telephoneNumber", "homePhone", "mobile"):
529 for number in self._get_phone_numbers(field):
530 ret.append(number)
6ff61434 531
e96e445b
MT
532 return ret
533
534 def set_phone_numbers(self, phone_numbers):
535 # Sort phone numbers by landline and mobile
536 _landline_numbers = []
537 _mobile_numbers = []
538
539 for number in phone_numbers:
540 try:
541 number = phonenumbers.parse(number, None)
542 except phonenumbers.phonenumberutil.NumberParseException:
543 continue
544
545 # Convert to string (in E.164 format)
546 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
547
548 # Separate mobile numbers
549 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
550 _mobile_numbers.append(s)
551 else:
552 _landline_numbers.append(s)
553
554 # Save
555 self._set_strings("telephoneNumber", _landline_numbers)
556 self._set_strings("mobile", _mobile_numbers)
557
558 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
559
560 @property
561 def _all_telephone_numbers(self):
6ccc8acb
MT
562 ret = [ self.sip_id, ]
563
d3208ac7
MT
564 if self.phone_number:
565 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
566 ret.append(s)
567
6ccc8acb
MT
568 for number in self.phone_numbers:
569 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
570 ret.append(s)
571
572 return ret
66862195 573
2cd9af74
MT
574 def avatar_url(self, size=None):
575 if self.backend.debug:
03706893 576 hostname = "http://people.dev.ipfire.org"
2cd9af74 577 else:
03706893 578 hostname = "https://people.ipfire.org"
2cd9af74 579
03706893 580 url = "%s/users/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
581
582 if size:
583 url += "?size=%s" % size
584
585 return url
586
2cd9af74 587 def get_avatar(self, size=None):
e96e445b 588 avatar = self._get_bytes("jpegPhoto")
2cd9af74
MT
589 if not avatar:
590 return
591
592 if not size:
593 return avatar
594
595 return self._resize_avatar(avatar, size)
596
597 def _resize_avatar(self, image, size):
1a226c83
MT
598 image = PIL.Image.open(io.BytesIO(image))
599
600 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
601 if image.mode == "RGBA":
602 image = image.convert("RGB")
603
22153577
MT
604 # Resize the image to the desired resolution (and make it square)
605 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
1a226c83
MT
606
607 with io.BytesIO() as f:
608 # If writing out the image does not work with optimization,
609 # we try to write it out without any optimization.
610 try:
22153577 611 thumbnail.save(f, "JPEG", optimize=True, quality=98)
1a226c83 612 except:
22153577 613 thumbnail.save(f, "JPEG", quality=98)
1a226c83
MT
614
615 return f.getvalue()
2cd9af74 616
5cc10421
MT
617 def upload_avatar(self, avatar):
618 self._set("jpegPhoto", avatar)
619
f4672785
MT
620 # SSH Keys
621
622 @lazy_property
623 def ssh_keys(self):
624 ret = []
625
626 for key in self._get_strings("sshPublicKey"):
627 s = sshpubkeys.SSHKey()
628
629 try:
630 s.parse(key)
631 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
632 logging.warning("Could not parse SSH key %s: %s" % (key, e))
633 continue
634
635 ret.append(s)
636
637 return ret
60024cc8 638
44b75370 639 def get_ssh_key_by_hash_sha256(self, hash_sha256):
55b67ca4 640 for key in self.ssh_keys:
44b75370 641 if not key.hash_sha256() == hash_sha256:
55b67ca4
MT
642 continue
643
644 return key
645
0d1fb712
MT
646 def add_ssh_key(self, key):
647 k = sshpubkeys.SSHKey()
648
649 # Try to parse the key
650 k.parse(key)
651
652 # Check for types and sufficient sizes
653 if k.key_type == b"ssh-rsa":
654 if k.bits < 4096:
655 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
656
657 elif k.key_type == b"ssh-dss":
658 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
659
660 # Ignore any duplicates
661 if key in (k.keydata for k in self.ssh_keys):
662 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
663 return
664
665 # Save key to LDAP
666 self._add_string("sshPublicKey", key)
667
668 # Append to cache
669 self.ssh_keys.append(k)
670
671 def delete_ssh_key(self, key):
672 if not key in (k.keydata for k in self.ssh_keys):
673 return
674
675 # Delete key from LDAP
676 self._delete_string("sshPublicKey", key)
677
55b67ca4 678
940227cb
MT
679if __name__ == "__main__":
680 a = Accounts()
681
11347e46 682 print(a.list())