]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
people: Show SSH keys for users
[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
16 from . import util
17 from .decorators import *
18 from .misc import Object
19
20 class Accounts(Object):
21 def __iter__(self):
22 # Only return developers (group with ID 1000)
23 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
24
25 return iter(sorted(accounts))
26
27 @lazy_property
28 def ldap(self):
29 # Connect to LDAP server
30 ldap_uri = self.settings.get("ldap_uri")
31 conn = ldap.initialize(ldap_uri)
32
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)
38
39 return conn
40
41 def _query(self, query, attrlist=None, limit=0):
42 logging.debug("Performing LDAP query: %s" % query)
43
44 search_base = self.settings.get("ldap_search_base")
45
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
51 self.ldap.close()
52 del self.ldap
53
54 raise
55
56 return results
57
58 def _search(self, query, attrlist=None, limit=0):
59 accounts = []
60
61 for dn, attrs in self._query(query, attrlist=attrlist, limit=limit):
62 account = Account(self.backend, dn, attrs)
63 accounts.append(account)
64
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:
75 for account in self._search("(&(objectClass=posixAccount)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
76 if not account in accounts:
77 accounts.append(account)
78
79 return sorted(accounts)
80
81 def _search_one(self, query):
82 result = self._search(query, limit=1)
83 assert len(result) <= 1
84
85 if result:
86 return result[0]
87
88 def get_by_uid(self, uid):
89 return self._search_one("(&(objectClass=posixAccount)(uid=%s))" % uid)
90
91 def get_by_mail(self, mail):
92 return self._search_one("(&(objectClass=posixAccount)(mail=%s))" % mail)
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)
102
103 def get_by_sip_id(self, sip_id):
104 return self._search_one("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
105 (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id, sip_id))
106
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
112 # Session stuff
113
114 def _cleanup_expired_sessions(self):
115 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
116
117 def create_session(self, account, host):
118 self._cleanup_expired_sessions()
119
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
155
156 class Account(Object):
157 def __init__(self, backend, dn, attrs=None):
158 Object.__init__(self, backend)
159 self.dn = dn
160
161 self.attributes = attrs or {}
162
163 def __str__(self):
164 return self.name
165
166 def __repr__(self):
167 return "<%s %s>" % (self.__class__.__name__, self.dn)
168
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
176
177 @property
178 def ldap(self):
179 return self.accounts.ldap
180
181 def _exists(self, key):
182 try:
183 self.attributes[key]
184 except KeyError:
185 return False
186
187 return True
188
189 def _get(self, key):
190 for value in self.attributes.get(key, []):
191 yield value
192
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 = []
228
229 if self._exists(key):
230 modlist.append((ldap.MOD_DELETE, key, None))
231
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,])
249
250 def passwd(self, new_password):
251 """
252 Sets a new password
253 """
254 self.ldap.passwd_s(self.dn, None, new_password)
255
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 """
263 logging.debug("Checking credentials for %s" % self.dn)
264
265 # Create a new LDAP connection
266 ldap_uri = self.backend.settings.get("ldap_uri")
267 conn = ldap.initialize(ldap_uri)
268
269 try:
270 conn.simple_bind_s(self.dn, password.encode("utf-8"))
271 except ldap.INVALID_CREDENTIALS:
272 logging.debug("Account credentials are invalid for %s" % self)
273 return False
274
275 logging.info("Successfully authenticated %s" % self)
276
277 return True
278
279 def is_admin(self):
280 return "wheel" in self.groups
281
282 def is_talk_enabled(self):
283 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
284 or self.telephone_numbers or self.address
285
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
297 @property
298 def classes(self):
299 return self._get_strings("objectClass")
300
301 @property
302 def uid(self):
303 return self._get_string("uid")
304
305 @property
306 def name(self):
307 return self._get_string("cn")
308
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)
334
335 @lazy_property
336 def groups(self):
337 groups = []
338
339 res = self.accounts._query("(&(objectClass=posixGroup) \
340 (memberUid=%s))" % self.uid, ["cn"])
341
342 for dn, attrs in res:
343 cns = attrs.get("cn")
344 if cns:
345 groups.append(cns[0].decode())
346
347 return groups
348
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(","))
356
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)
365
366 @property
367 def email(self):
368 name = self.name.lower()
369 name = name.replace(" ", ".")
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")
376
377 for mail in self.attributes.get("mail", []):
378 if mail.decode().startswith("%s@ipfire.org" % name):
379 return mail
380
381 # If everything else fails, we will go with the UID
382 return "%s@ipfire.org" % self.uid
383
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
394 @property
395 def sip_id(self):
396 if "sipUser" in self.classes:
397 return self._get_string("sipAuthenticationUser")
398
399 if "sipRoutingObject" in self.classes:
400 return self._get_string("sipLocalAddress")
401
402 @property
403 def sip_password(self):
404 return self._get_string("sipPassword")
405
406 @staticmethod
407 def _generate_sip_password():
408 return util.random_string(8)
409
410 @property
411 def sip_url(self):
412 return "%s@ipfire.org" % self.sip_id
413
414 def uses_sip_forwarding(self):
415 if self.sip_routing_address:
416 return True
417
418 return False
419
420 # SIP Routing
421
422 def get_sip_routing_address(self):
423 if "sipRoutingObject" in self.classes:
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)
462
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
474 @lazy_property
475 def sip_channels(self):
476 return self.backend.talk.freeswitch.get_sip_channels(self)
477
478 def get_cdr(self, date=None, limit=None):
479 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
480
481 # Phone Numbers
482
483 def get_phone_numbers(self):
484 ret = []
485
486 for field in ("telephoneNumber", "homePhone", "mobile"):
487 for number in self._get_phone_numbers(field):
488 ret.append(number)
489
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)
517
518 @property
519 def _all_telephone_numbers(self):
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
527
528 def avatar_url(self, size=None):
529 if self.backend.debug:
530 hostname = "http://people.dev.ipfire.org"
531 else:
532 hostname = "https://people.ipfire.org"
533
534 url = "%s/users/%s.jpg" % (hostname, self.uid)
535
536 if size:
537 url += "?size=%s" % size
538
539 return url
540
541 def get_avatar(self, size=None):
542 avatar = self._get_bytes("jpegPhoto")
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):
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
558 # Resize the image to the desired resolution (and make it square)
559 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
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:
565 thumbnail.save(f, "JPEG", optimize=True, quality=98)
566 except:
567 thumbnail.save(f, "JPEG", quality=98)
568
569 return f.getvalue()
570
571 def upload_avatar(self, avatar):
572 self._set("jpegPhoto", avatar)
573
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
592
593 if __name__ == "__main__":
594 a = Accounts()
595
596 print(a.list())