]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
people: Show SSH keys for users
[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
3ea97943
MT
250 def passwd(self, new_password):
251 """
252 Sets a new password
253 """
254 self.ldap.passwd_s(self.dn, None, new_password)
255
940227cb
MT
256 def check_password(self, password):
257 """
258 Bind to the server with given credentials and return
259 true if password is corrent and false if not.
260
261 Raises exceptions from the server on any other errors.
262 """
940227cb 263 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
264
265 # Create a new LDAP connection
266 ldap_uri = self.backend.settings.get("ldap_uri")
267 conn = ldap.initialize(ldap_uri)
268
940227cb 269 try:
3ea97943 270 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 271 except ldap.INVALID_CREDENTIALS:
3ea97943 272 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
273 return False
274
3ea97943
MT
275 logging.info("Successfully authenticated %s" % self)
276
940227cb
MT
277 return True
278
940227cb 279 def is_admin(self):
d82bc8e3 280 return "wheel" in self.groups
66862195
MT
281
282 def is_talk_enabled(self):
06c1d39c
MT
283 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
284 or self.telephone_numbers or self.address
66862195 285
e96e445b
MT
286 def can_be_managed_by(self, account):
287 """
288 Returns True if account is allowed to manage this account
289 """
290 # Admins can manage all accounts
291 if account.is_admin():
292 return True
293
294 # Users can manage themselves
295 return self == account
296
66862195
MT
297 @property
298 def classes(self):
e96e445b 299 return self._get_strings("objectClass")
66862195
MT
300
301 @property
302 def uid(self):
e96e445b 303 return self._get_string("uid")
940227cb 304
a6dc0bad
MT
305 @property
306 def name(self):
e96e445b 307 return self._get_string("cn")
66862195 308
e96e445b
MT
309 # First Name
310
311 def get_first_name(self):
312 return self._get_string("givenName")
313
314 def set_first_name(self, first_name):
315 self._set_string("givenName", first_name)
316
317 # Update Common Name
318 self._set_string("cn", "%s %s" % (first_name, self.last_name))
319
320 first_name = property(get_first_name, set_first_name)
321
322 # Last Name
323
324 def get_last_name(self):
325 return self._get_string("sn")
326
327 def set_last_name(self, last_name):
328 self._set_string("sn", last_name)
329
330 # Update Common Name
331 self._set_string("cn", "%s %s" % (self.first_name, last_name))
332
333 last_name = property(get_last_name, set_last_name)
66862195 334
1bae74c7 335 @lazy_property
66862195 336 def groups(self):
1bae74c7 337 groups = []
66862195 338
1bae74c7
MT
339 res = self.accounts._query("(&(objectClass=posixGroup) \
340 (memberUid=%s))" % self.uid, ["cn"])
66862195 341
1bae74c7
MT
342 for dn, attrs in res:
343 cns = attrs.get("cn")
344 if cns:
345 groups.append(cns[0].decode())
66862195 346
1bae74c7 347 return groups
66862195 348
e96e445b
MT
349 # Address
350
351 def get_address(self):
352 address = self._get_string("homePostalAddress")
353
354 if address:
355 return (line.strip() for line in address.split(","))
66862195 356
e96e445b
MT
357 return []
358
359 def set_address(self, address):
360 data = ", ".join(address.splitlines())
361
362 self._set_bytes("homePostalAddress", data.encode())
363
364 address = property(get_address, set_address)
a6dc0bad 365
940227cb
MT
366 @property
367 def email(self):
66862195 368 name = self.name.lower()
940227cb 369 name = name.replace(" ", ".")
78fdedae
MT
370 name = name.replace("Ä", "Ae")
371 name = name.replace("Ö", "Oe")
372 name = name.replace("Ü", "Ue")
373 name = name.replace("ä", "ae")
374 name = name.replace("ö", "oe")
375 name = name.replace("ü", "ue")
940227cb 376
66862195 377 for mail in self.attributes.get("mail", []):
7aee4b8d 378 if mail.decode().startswith("%s@ipfire.org" % name):
940227cb
MT
379 return mail
380
2cd9af74
MT
381 # If everything else fails, we will go with the UID
382 return "%s@ipfire.org" % self.uid
940227cb 383
e96e445b
MT
384 # Mail Routing Address
385
386 def get_mail_routing_address(self):
387 return self._get_string("mailRoutingAddress", None)
388
389 def set_mail_routing_address(self, address):
390 self._set_string("mailRoutingAddress", address)
391
392 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
393
66862195
MT
394 @property
395 def sip_id(self):
396 if "sipUser" in self.classes:
e96e445b 397 return self._get_string("sipAuthenticationUser")
66862195
MT
398
399 if "sipRoutingObject" in self.classes:
e96e445b 400 return self._get_string("sipLocalAddress")
66862195 401
2f51147a
MT
402 @property
403 def sip_password(self):
e96e445b
MT
404 return self._get_string("sipPassword")
405
406 @staticmethod
407 def _generate_sip_password():
408 return util.random_string(8)
2f51147a 409
66862195
MT
410 @property
411 def sip_url(self):
412 return "%s@ipfire.org" % self.sip_id
413
414 def uses_sip_forwarding(self):
e96e445b 415 if self.sip_routing_address:
66862195
MT
416 return True
417
418 return False
419
e96e445b
MT
420 # SIP Routing
421
422 def get_sip_routing_address(self):
66862195 423 if "sipRoutingObject" in self.classes:
e96e445b
MT
424 return self._get_string("sipRoutingAddress")
425
426 def set_sip_routing_address(self, address):
427 if not address:
428 address = None
429
430 # Don't do anything if nothing has changed
431 if self.get_sip_routing_address() == address:
432 return
433
434 if address:
435 modlist = [
436 # This is no longer a SIP user any more
437 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
438 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
439 (ldap.MOD_DELETE, "sipPassword", None),
440
441 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
442 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
443 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
444 ]
445 else:
446 modlist = [
447 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
448 (ldap.MOD_DELETE, "sipLocalAddress", None),
449 (ldap.MOD_DELETE, "sipRoutingAddress", None),
450
451 (ldap.MOD_ADD, "objectClass", b"sipUser"),
452 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
453 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
454 ]
455
456 # Run modification
457 self._modify(modlist)
458
459 # XXX Cache is invalid here
460
461 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 462
917434b8
MT
463 @lazy_property
464 def sip_registrations(self):
465 sip_registrations = []
466
467 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
468 reg.account = self
469
470 sip_registrations.append(reg)
471
472 return sip_registrations
473
1f38be5a
MT
474 @lazy_property
475 def sip_channels(self):
476 return self.backend.talk.freeswitch.get_sip_channels(self)
477
bdaf6b46
MT
478 def get_cdr(self, date=None, limit=None):
479 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 480
e96e445b 481 # Phone Numbers
6ff61434 482
e96e445b
MT
483 def get_phone_numbers(self):
484 ret = []
6ff61434 485
e96e445b
MT
486 for field in ("telephoneNumber", "homePhone", "mobile"):
487 for number in self._get_phone_numbers(field):
488 ret.append(number)
6ff61434 489
e96e445b
MT
490 return ret
491
492 def set_phone_numbers(self, phone_numbers):
493 # Sort phone numbers by landline and mobile
494 _landline_numbers = []
495 _mobile_numbers = []
496
497 for number in phone_numbers:
498 try:
499 number = phonenumbers.parse(number, None)
500 except phonenumbers.phonenumberutil.NumberParseException:
501 continue
502
503 # Convert to string (in E.164 format)
504 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
505
506 # Separate mobile numbers
507 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
508 _mobile_numbers.append(s)
509 else:
510 _landline_numbers.append(s)
511
512 # Save
513 self._set_strings("telephoneNumber", _landline_numbers)
514 self._set_strings("mobile", _mobile_numbers)
515
516 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
517
518 @property
519 def _all_telephone_numbers(self):
6ccc8acb
MT
520 ret = [ self.sip_id, ]
521
522 for number in self.phone_numbers:
523 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
524 ret.append(s)
525
526 return ret
66862195 527
2cd9af74
MT
528 def avatar_url(self, size=None):
529 if self.backend.debug:
03706893 530 hostname = "http://people.dev.ipfire.org"
2cd9af74 531 else:
03706893 532 hostname = "https://people.ipfire.org"
2cd9af74 533
03706893 534 url = "%s/users/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
535
536 if size:
537 url += "?size=%s" % size
538
539 return url
540
2cd9af74 541 def get_avatar(self, size=None):
e96e445b 542 avatar = self._get_bytes("jpegPhoto")
2cd9af74
MT
543 if not avatar:
544 return
545
546 if not size:
547 return avatar
548
549 return self._resize_avatar(avatar, size)
550
551 def _resize_avatar(self, image, size):
1a226c83
MT
552 image = PIL.Image.open(io.BytesIO(image))
553
554 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
555 if image.mode == "RGBA":
556 image = image.convert("RGB")
557
22153577
MT
558 # Resize the image to the desired resolution (and make it square)
559 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
1a226c83
MT
560
561 with io.BytesIO() as f:
562 # If writing out the image does not work with optimization,
563 # we try to write it out without any optimization.
564 try:
22153577 565 thumbnail.save(f, "JPEG", optimize=True, quality=98)
1a226c83 566 except:
22153577 567 thumbnail.save(f, "JPEG", quality=98)
1a226c83
MT
568
569 return f.getvalue()
2cd9af74 570
5cc10421
MT
571 def upload_avatar(self, avatar):
572 self._set("jpegPhoto", avatar)
573
f4672785
MT
574 # SSH Keys
575
576 @lazy_property
577 def ssh_keys(self):
578 ret = []
579
580 for key in self._get_strings("sshPublicKey"):
581 s = sshpubkeys.SSHKey()
582
583 try:
584 s.parse(key)
585 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
586 logging.warning("Could not parse SSH key %s: %s" % (key, e))
587 continue
588
589 ret.append(s)
590
591 return ret
60024cc8 592
940227cb
MT
593if __name__ == "__main__":
594 a = Accounts()
595
11347e46 596 print(a.list())