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