]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
Revert "auth: Wrap login and logout in a single transaction"
[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
f0c9d237 13import time
eea71144
MT
14import urllib.parse
15import urllib.request
6b582a4f 16import zxcvbn
940227cb 17
0099c2a7 18from . import countries
e96e445b 19from . import util
917434b8 20from .decorators import *
11347e46 21from .misc import Object
940227cb 22
a6dc0bad 23class Accounts(Object):
58d22b5d
MT
24 def init(self):
25 self.search_base = self.settings.get("ldap_search_base")
26
9f05796c
MT
27 def __iter__(self):
28 # Only return developers (group with ID 1000)
1bae74c7 29 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
9f05796c 30
1bae74c7 31 return iter(sorted(accounts))
9f05796c 32
0ab42c1d 33 @lazy_property
66862195 34 def ldap(self):
0ab42c1d
MT
35 # Connect to LDAP server
36 ldap_uri = self.settings.get("ldap_uri")
940227cb 37
867c06a1
MT
38 logging.debug("Connecting to LDAP server: %s" % ldap_uri)
39
6c9a8663
MT
40 # Connect to the LDAP server
41 conn = ldap.ldapobject.ReconnectLDAPObject(ldap_uri,
42 retry_max=10, retry_delay=3)
43
0ab42c1d
MT
44 # Bind with username and password
45 bind_dn = self.settings.get("ldap_bind_dn")
46 if bind_dn:
47 bind_pw = self.settings.get("ldap_bind_pw", "")
48 conn.simple_bind(bind_dn, bind_pw)
66862195 49
0ab42c1d 50 return conn
940227cb 51
91f72160 52 def _query(self, query, attrlist=None, limit=0, search_base=None, scope=None):
66862195 53 logging.debug("Performing LDAP query: %s" % query)
940227cb 54
f0c9d237 55 t = time.time()
a69e87a1 56
91f72160
MT
57 results = self.ldap.search_ext_s(search_base or self.search_base,
58 scope or ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit)
f0c9d237
MT
59
60 # Log time it took to perform the query
61 logging.debug("Query took %.2fms" % ((time.time() - t) * 1000.0))
940227cb 62
66862195 63 return results
940227cb 64
1bae74c7 65 def _search(self, query, attrlist=None, limit=0):
91f72160
MT
66 dns = self._search_dns(query, limit=limit)
67
0dcf4344
MT
68 accounts = self._get_accounts_from_dns(dns)
69
70 return list(accounts)
91f72160
MT
71
72 def _search_dns(self, query, limit=0):
73 # Query cache
74 dns = self.memcache.get("accounts:search:%s" % query)
75 if dns is None:
76 # Return DNs only for this search query
77 dns = []
78 for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
79 dns.append(dn)
80
81 # Cache for 10 min
82 self.memcache.set("accounts:search:%s" % query, dns, 600)
83
84 return dns
85
86 def _get_accounts_from_dns(self, dns):
87 for dn in dns:
2cdf68d8 88 yield self.get_by_dn(dn)
91f72160 89
2cdf68d8 90 def get_by_dn(self, dn):
91f72160
MT
91 attrs = self.memcache.get("accounts:%s:attrs" % dn)
92 if attrs is None:
93 attrs = self._get_attrs(dn)
94 assert attrs, dn
95
96 # Cache all attributes for 5 min
97 self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
98
99 return Account(self.backend, dn, attrs)
100
101 def _get_attrs(self, dn):
102 """
103 Fetches all attributes for the given distinguished name
104 """
105 results = self._query("(objectClass=*)", search_base=dn, limit=1)
106
107 for dn, attrs in results:
108 return attrs
1bae74c7
MT
109
110 def search(self, query):
111 # Search for exact matches
df70e85e
MT
112 accounts = self._search(
113 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
1bae74c7
MT
114 % (query, query, query, query, query, query))
115
116 # Find accounts by name
117 if not accounts:
73a54cb6 118 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
1bae74c7
MT
119 if not account in accounts:
120 accounts.append(account)
121
66862195
MT
122 return sorted(accounts)
123
1bae74c7 124 def _search_one(self, query):
18209c78 125 results = self._search(query, limit=1)
66862195 126
18209c78
MT
127 for result in results:
128 return result
66862195 129
f32dd17f
MT
130 def uid_exists(self, uid):
131 if self.get_by_uid(uid):
132 return True
133
134 res = self.db.get("SELECT 1 FROM account_activations \
135 WHERE uid = %s AND expires_at > NOW()", uid)
136
137 if res:
138 return True
139
140 # Account with uid does not exist, yet
141 return False
142
66862195 143 def get_by_uid(self, uid):
73a54cb6 144 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
66862195
MT
145
146 def get_by_mail(self, mail):
73a54cb6 147 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
66862195 148
66862195
MT
149 def find_account(self, s):
150 account = self.get_by_uid(s)
151 if account:
152 return account
153
154 return self.get_by_mail(s)
940227cb 155
66862195 156 def get_by_sip_id(self, sip_id):
df70e85e
MT
157 if not sip_id:
158 return
159
160 return self._search_one(
161 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
162 % (sip_id, sip_id))
940227cb 163
525c01f7 164 def get_by_phone_number(self, number):
df70e85e
MT
165 if not number:
166 return
167
168 return self._search_one(
169 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
525c01f7
MT
170 % (number, number, number, number))
171
f32dd17f
MT
172 # Registration
173
718d1375 174 def register(self, uid, email, first_name, last_name):
f32dd17f
MT
175 # Check if UID is unique
176 if self.get_by_uid(uid):
177 raise ValueError("UID exists: %s" % uid)
178
718d1375
MT
179 # Generate a random activation code
180 activation_code = util.random_string(36)
181
182 # Create an entry in our database until the user
183 # has activated the account
184 self.db.execute("INSERT INTO account_activations(uid, activation_code, \
185 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
186 uid, activation_code, email, first_name, last_name)
187
188 # Send an account activation email
189 self.backend.messages.send_template("auth/messages/register",
190 recipients=[email], priority=100, uid=uid,
191 activation_code=activation_code, email=email,
192 first_name=first_name, last_name=last_name)
193
b4d72c76
MT
194 def activate(self, uid, activation_code):
195 res = self.db.get("DELETE FROM account_activations \
196 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
197 RETURNING *", uid, activation_code)
198
199 # Return nothing when account was not found
200 if not res:
201 return
f32dd17f 202
b4d72c76
MT
203 # Create a new account on the LDAP database
204 return self.create(uid, res.email,
205 first_name=res.first_name, last_name=res.last_name)
206
207 def create(self, uid, email, first_name, last_name):
f32dd17f
MT
208 # Account Parameters
209 account = {
210 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
f32dd17f
MT
211 "mail" : email.encode(),
212
213 # Name
214 "cn" : b"%s %s" % (first_name.encode(), last_name.encode()),
215 "sn" : last_name.encode(),
216 "givenName" : first_name.encode(),
217 }
218
b4d72c76
MT
219 logging.info("Creating new account: %s: %s" % (uid, account))
220
221 # Create DN
222 dn = "uid=%s,ou=People,dc=mcfly,dc=local" % uid
223
f32dd17f 224 # Create account on LDAP
b4d72c76 225 self.ldap.add_s(dn, ldap.modlist.addModlist(account))
f32dd17f 226
b4d72c76 227 # Return account
2cdf68d8 228 return self.get_by_dn(dn)
f32dd17f 229
66862195 230 # Session stuff
940227cb 231
66862195
MT
232 def _cleanup_expired_sessions(self):
233 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
940227cb 234
66862195
MT
235 def create_session(self, account, host):
236 self._cleanup_expired_sessions()
940227cb 237
66862195
MT
238 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
239 RETURNING session_id, time_expires", host, account.uid)
240
241 # Session could not be created
242 if not res:
243 return None, None
244
245 logging.info("Created session %s for %s which expires %s" \
246 % (res.session_id, account, res.time_expires))
247 return res.session_id, res.time_expires
248
249 def destroy_session(self, session_id, host):
250 logging.info("Destroying session %s" % session_id)
251
252 self.db.execute("DELETE FROM sessions \
253 WHERE session_id = %s AND host = %s", session_id, host)
254 self._cleanup_expired_sessions()
255
256 def get_by_session(self, session_id, host):
257 logging.debug("Looking up session %s" % session_id)
258
259 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
260 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
261 session_id, host)
262
263 # Session does not exist or has expired
264 if not res:
265 return
266
267 # Update the session expiration time
268 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
269 WHERE session_id = %s AND host = %s", session_id, host)
270
271 return self.get_by_uid(res.uid)
d86f6f18 272
940227cb 273
a6dc0bad 274class Account(Object):
66862195 275 def __init__(self, backend, dn, attrs=None):
a6dc0bad 276 Object.__init__(self, backend)
940227cb
MT
277 self.dn = dn
278
e96e445b 279 self.attributes = attrs or {}
940227cb 280
917434b8
MT
281 def __str__(self):
282 return self.name
283
940227cb
MT
284 def __repr__(self):
285 return "<%s %s>" % (self.__class__.__name__, self.dn)
286
541c952b
MT
287 def __eq__(self, other):
288 if isinstance(other, self.__class__):
289 return self.dn == other.dn
290
291 def __lt__(self, other):
292 if isinstance(other, self.__class__):
293 return self.name < other.name
940227cb
MT
294
295 @property
66862195
MT
296 def ldap(self):
297 return self.accounts.ldap
940227cb 298
e96e445b
MT
299 def _exists(self, key):
300 try:
301 self.attributes[key]
302 except KeyError:
303 return False
940227cb 304
e96e445b 305 return True
940227cb 306
e96e445b
MT
307 def _get(self, key):
308 for value in self.attributes.get(key, []):
309 yield value
940227cb 310
e96e445b
MT
311 def _get_bytes(self, key, default=None):
312 for value in self._get(key):
313 return value
314
315 return default
316
317 def _get_strings(self, key):
318 for value in self._get(key):
319 yield value.decode()
320
321 def _get_string(self, key, default=None):
322 for value in self._get_strings(key):
323 return value
324
325 return default
326
327 def _get_phone_numbers(self, key):
328 for value in self._get_strings(key):
329 yield phonenumbers.parse(value, None)
330
331 def _modify(self, modlist):
332 logging.debug("Modifying %s: %s" % (self.dn, modlist))
333
334 # Run modify operation
335 self.ldap.modify_s(self.dn, modlist)
336
91f72160
MT
337 # Delete cached attributes
338 self.memcache.delete("accounts:%s:attrs")
339
e96e445b
MT
340 def _set(self, key, values):
341 current = self._get(key)
342
343 # Don't do anything if nothing has changed
344 if list(current) == values:
345 return
346
347 # Remove all old values and add all new ones
348 modlist = []
940227cb 349
e96e445b
MT
350 if self._exists(key):
351 modlist.append((ldap.MOD_DELETE, key, None))
940227cb 352
e96e445b 353 # Add new values
47bb098f
MT
354 if values:
355 modlist.append((ldap.MOD_ADD, key, values))
e96e445b
MT
356
357 # Run modify operation
358 self._modify(modlist)
359
360 # Update cache
361 self.attributes.update({ key : values })
362
363 def _set_bytes(self, key, values):
364 return self._set(key, values)
365
366 def _set_strings(self, key, values):
47bb098f 367 return self._set(key, [e.encode() for e in values if e])
e96e445b
MT
368
369 def _set_string(self, key, value):
370 return self._set_strings(key, [value,])
940227cb 371
0d1fb712
MT
372 def _add(self, key, values):
373 modlist = [
374 (ldap.MOD_ADD, key, values),
375 ]
376
377 self._modify(modlist)
378
379 def _add_strings(self, key, values):
380 return self._add(key, [e.encode() for e in values])
381
382 def _add_string(self, key, value):
383 return self._add_strings(key, [value,])
384
385 def _delete(self, key, values):
386 modlist = [
387 (ldap.MOD_DELETE, key, values),
388 ]
389
390 self._modify(modlist)
391
392 def _delete_strings(self, key, values):
393 return self._delete(key, [e.encode() for e in values])
394
395 def _delete_string(self, key, value):
396 return self._delete_strings(key, [value,])
397
6b582a4f 398 def passwd(self, password):
3ea97943
MT
399 """
400 Sets a new password
401 """
6b582a4f
MT
402 # The new password must have a score of 3 or better
403 quality = self.check_password_quality(password)
404 if quality["score"] < 3:
405 raise ValueError("Password too weak")
406
407 self.ldap.passwd_s(self.dn, None, password)
3ea97943 408
940227cb
MT
409 def check_password(self, password):
410 """
411 Bind to the server with given credentials and return
412 true if password is corrent and false if not.
413
414 Raises exceptions from the server on any other errors.
415 """
0d1fb712
MT
416 if not password:
417 return
418
940227cb 419 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
420
421 # Create a new LDAP connection
422 ldap_uri = self.backend.settings.get("ldap_uri")
423 conn = ldap.initialize(ldap_uri)
424
940227cb 425 try:
3ea97943 426 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 427 except ldap.INVALID_CREDENTIALS:
3ea97943 428 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
429 return False
430
3ea97943
MT
431 logging.info("Successfully authenticated %s" % self)
432
940227cb
MT
433 return True
434
6b582a4f
MT
435 def check_password_quality(self, password):
436 """
437 Passwords are passed through zxcvbn to make sure
438 that they are strong enough.
439 """
440 return zxcvbn.zxcvbn(password, user_inputs=(
441 self.first_name, self.last_name,
442 ))
443
940227cb 444 def is_admin(self):
d82bc8e3 445 return "wheel" in self.groups
66862195 446
71a3109c
MT
447 def is_staff(self):
448 return "staff" in self.groups
449
450 def has_shell(self):
451 return "posixAccount" in self.classes
452
453 def has_mail(self):
454 return "postfixMailUser" in self.classes
455
456 def has_sip(self):
457 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
66862195 458
e96e445b
MT
459 def can_be_managed_by(self, account):
460 """
461 Returns True if account is allowed to manage this account
462 """
463 # Admins can manage all accounts
464 if account.is_admin():
465 return True
466
467 # Users can manage themselves
468 return self == account
469
66862195
MT
470 @property
471 def classes(self):
e96e445b 472 return self._get_strings("objectClass")
66862195
MT
473
474 @property
475 def uid(self):
e96e445b 476 return self._get_string("uid")
940227cb 477
a6dc0bad
MT
478 @property
479 def name(self):
e96e445b 480 return self._get_string("cn")
66862195 481
e96e445b
MT
482 # First Name
483
484 def get_first_name(self):
485 return self._get_string("givenName")
486
487 def set_first_name(self, first_name):
488 self._set_string("givenName", first_name)
489
490 # Update Common Name
491 self._set_string("cn", "%s %s" % (first_name, self.last_name))
492
493 first_name = property(get_first_name, set_first_name)
494
495 # Last Name
496
497 def get_last_name(self):
498 return self._get_string("sn")
499
500 def set_last_name(self, last_name):
501 self._set_string("sn", last_name)
502
503 # Update Common Name
504 self._set_string("cn", "%s %s" % (self.first_name, last_name))
505
506 last_name = property(get_last_name, set_last_name)
66862195 507
1bae74c7 508 @lazy_property
66862195 509 def groups(self):
819daf36
MT
510 groups = self.memcache.get("accounts:%s:groups" % self.dn)
511 if groups:
512 return groups
513
514 # Fetch groups from LDAP
515 groups = self._get_groups()
516
517 # Cache groups for 5 min
518 self.memcache.set("accounts:%s:groups" % self.dn, groups, 300)
519
520 return groups
521
522 def _get_groups(self):
1bae74c7 523 groups = []
66862195 524
1bae74c7
MT
525 res = self.accounts._query("(&(objectClass=posixGroup) \
526 (memberUid=%s))" % self.uid, ["cn"])
66862195 527
1bae74c7
MT
528 for dn, attrs in res:
529 cns = attrs.get("cn")
530 if cns:
531 groups.append(cns[0].decode())
66862195 532
1bae74c7 533 return groups
66862195 534
e96e445b
MT
535 # Address
536
0099c2a7
MT
537 @property
538 def address(self):
539 address = []
540
541 if self.street:
542 address += self.street.splitlines()
543
544 if self.postal_code and self.city:
545 if self.country_code in ("AT", "DE"):
546 address.append("%s %s" % (self.postal_code, self.city))
547 else:
548 address.append("%s, %s" % (self.city, self.postal_code))
549 else:
550 address.append(self.city or self.postal_code)
551
552 if self.country_name:
553 address.append(self.country_name)
554
555 return address
556
557 def get_street(self):
558 return self._get_string("street") or self._get_string("homePostalAddress")
559
560 def set_street(self, street):
561 self._set_string("street", street)
e96e445b 562
0099c2a7 563 street = property(get_street, set_street)
66862195 564
0099c2a7
MT
565 def get_city(self):
566 return self._get_string("l") or ""
e96e445b 567
0099c2a7
MT
568 def set_city(self, city):
569 self._set_string("l", city)
e96e445b 570
0099c2a7 571 city = property(get_city, set_city)
e96e445b 572
0099c2a7
MT
573 def get_postal_code(self):
574 return self._get_string("postalCode") or ""
575
576 def set_postal_code(self, postal_code):
577 self._set_string("postalCode", postal_code)
578
579 postal_code = property(get_postal_code, set_postal_code)
580
581 # XXX This should be c
582 def get_country_code(self):
583 return self._get_string("st")
584
585 def set_country_code(self, country_code):
586 self._set_string("st", country_code)
587
588 country_code = property(get_country_code, set_country_code)
589
590 @property
591 def country_name(self):
592 if self.country_code:
593 return countries.get_name(self.country_code)
a6dc0bad 594
940227cb
MT
595 @property
596 def email(self):
d86f6f18 597 return self._get_string("mail")
940227cb 598
e96e445b
MT
599 # Mail Routing Address
600
601 def get_mail_routing_address(self):
602 return self._get_string("mailRoutingAddress", None)
603
604 def set_mail_routing_address(self, address):
47bb098f 605 self._set_string("mailRoutingAddress", address or None)
e96e445b
MT
606
607 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
608
66862195
MT
609 @property
610 def sip_id(self):
611 if "sipUser" in self.classes:
e96e445b 612 return self._get_string("sipAuthenticationUser")
66862195
MT
613
614 if "sipRoutingObject" in self.classes:
e96e445b 615 return self._get_string("sipLocalAddress")
66862195 616
2f51147a
MT
617 @property
618 def sip_password(self):
e96e445b
MT
619 return self._get_string("sipPassword")
620
621 @staticmethod
622 def _generate_sip_password():
623 return util.random_string(8)
2f51147a 624
66862195
MT
625 @property
626 def sip_url(self):
627 return "%s@ipfire.org" % self.sip_id
628
629 def uses_sip_forwarding(self):
e96e445b 630 if self.sip_routing_address:
66862195
MT
631 return True
632
633 return False
634
e96e445b
MT
635 # SIP Routing
636
637 def get_sip_routing_address(self):
66862195 638 if "sipRoutingObject" in self.classes:
e96e445b
MT
639 return self._get_string("sipRoutingAddress")
640
641 def set_sip_routing_address(self, address):
642 if not address:
643 address = None
644
645 # Don't do anything if nothing has changed
646 if self.get_sip_routing_address() == address:
647 return
648
649 if address:
79cce555
MT
650 # This is no longer a SIP user any more
651 try:
652 self._modify([
653 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
654 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
655 (ldap.MOD_DELETE, "sipPassword", None),
656 ])
657 except ldap.NO_SUCH_ATTRIBUTE:
658 pass
659
660 # Set new routing object
661 try:
662 self._modify([
663 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
664 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
665 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
666 ])
667
668 # If this is a change, we cannot add this again
669 except ldap.TYPE_OR_VALUE_EXISTS:
670 self._set_string("sipRoutingAddress", address)
e96e445b 671 else:
79cce555
MT
672 try:
673 self._modify([
674 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
675 (ldap.MOD_DELETE, "sipLocalAddress", None),
676 (ldap.MOD_DELETE, "sipRoutingAddress", None),
677 ])
678 except ldap.NO_SUCH_ATTRIBUTE:
679 pass
680
681 self._modify([
e96e445b
MT
682 (ldap.MOD_ADD, "objectClass", b"sipUser"),
683 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
684 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
79cce555 685 ])
e96e445b
MT
686
687 # XXX Cache is invalid here
688
689 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 690
917434b8
MT
691 @lazy_property
692 def sip_registrations(self):
693 sip_registrations = []
694
695 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
696 reg.account = self
697
698 sip_registrations.append(reg)
699
700 return sip_registrations
701
1f38be5a
MT
702 @lazy_property
703 def sip_channels(self):
704 return self.backend.talk.freeswitch.get_sip_channels(self)
705
bdaf6b46
MT
706 def get_cdr(self, date=None, limit=None):
707 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 708
e96e445b 709 # Phone Numbers
6ff61434 710
d3208ac7
MT
711 @lazy_property
712 def phone_number(self):
713 """
714 Returns the IPFire phone number
715 """
716 if self.sip_id:
717 return phonenumbers.parse("+4923636035%s" % self.sip_id)
718
719 @lazy_property
720 def fax_number(self):
721 if self.sip_id:
722 return phonenumbers.parse("+49236360359%s" % self.sip_id)
723
e96e445b
MT
724 def get_phone_numbers(self):
725 ret = []
6ff61434 726
e96e445b
MT
727 for field in ("telephoneNumber", "homePhone", "mobile"):
728 for number in self._get_phone_numbers(field):
729 ret.append(number)
6ff61434 730
e96e445b
MT
731 return ret
732
733 def set_phone_numbers(self, phone_numbers):
734 # Sort phone numbers by landline and mobile
735 _landline_numbers = []
736 _mobile_numbers = []
737
738 for number in phone_numbers:
739 try:
740 number = phonenumbers.parse(number, None)
741 except phonenumbers.phonenumberutil.NumberParseException:
742 continue
743
744 # Convert to string (in E.164 format)
745 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
746
747 # Separate mobile numbers
748 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
749 _mobile_numbers.append(s)
750 else:
751 _landline_numbers.append(s)
752
753 # Save
754 self._set_strings("telephoneNumber", _landline_numbers)
755 self._set_strings("mobile", _mobile_numbers)
756
757 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
758
759 @property
760 def _all_telephone_numbers(self):
6ccc8acb
MT
761 ret = [ self.sip_id, ]
762
d3208ac7
MT
763 if self.phone_number:
764 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
765 ret.append(s)
766
6ccc8acb
MT
767 for number in self.phone_numbers:
768 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
769 ret.append(s)
770
771 return ret
66862195 772
2cd9af74
MT
773 def avatar_url(self, size=None):
774 if self.backend.debug:
03706893 775 hostname = "http://people.dev.ipfire.org"
2cd9af74 776 else:
03706893 777 hostname = "https://people.ipfire.org"
2cd9af74 778
03706893 779 url = "%s/users/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
780
781 if size:
782 url += "?size=%s" % size
783
784 return url
785
2cd9af74 786 def get_avatar(self, size=None):
e96e445b 787 avatar = self._get_bytes("jpegPhoto")
2cd9af74
MT
788 if not avatar:
789 return
790
791 if not size:
792 return avatar
793
794 return self._resize_avatar(avatar, size)
795
796 def _resize_avatar(self, image, size):
1a226c83
MT
797 image = PIL.Image.open(io.BytesIO(image))
798
22153577
MT
799 # Resize the image to the desired resolution (and make it square)
800 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
1a226c83
MT
801
802 with io.BytesIO() as f:
803 # If writing out the image does not work with optimization,
804 # we try to write it out without any optimization.
805 try:
020de883 806 thumbnail.save(f, image.format, optimize=True, quality=98)
1a226c83 807 except:
020de883 808 thumbnail.save(f, image.format, quality=98)
1a226c83
MT
809
810 return f.getvalue()
2cd9af74 811
5cc10421
MT
812 def upload_avatar(self, avatar):
813 self._set("jpegPhoto", avatar)
814
f4672785
MT
815 # SSH Keys
816
817 @lazy_property
818 def ssh_keys(self):
819 ret = []
820
821 for key in self._get_strings("sshPublicKey"):
822 s = sshpubkeys.SSHKey()
823
824 try:
825 s.parse(key)
826 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
827 logging.warning("Could not parse SSH key %s: %s" % (key, e))
828 continue
829
830 ret.append(s)
831
832 return ret
60024cc8 833
44b75370 834 def get_ssh_key_by_hash_sha256(self, hash_sha256):
55b67ca4 835 for key in self.ssh_keys:
44b75370 836 if not key.hash_sha256() == hash_sha256:
55b67ca4
MT
837 continue
838
839 return key
840
0d1fb712
MT
841 def add_ssh_key(self, key):
842 k = sshpubkeys.SSHKey()
843
844 # Try to parse the key
845 k.parse(key)
846
847 # Check for types and sufficient sizes
848 if k.key_type == b"ssh-rsa":
849 if k.bits < 4096:
850 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
851
852 elif k.key_type == b"ssh-dss":
853 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
854
855 # Ignore any duplicates
856 if key in (k.keydata for k in self.ssh_keys):
857 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
858 return
859
cc27cb63
MT
860 # Prepare transaction
861 modlist = []
862
863 # Add object class if user is not in it, yet
864 if not "ldapPublicKey" in self.classes:
865 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
866
867 # Add key
868 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
869
0d1fb712 870 # Save key to LDAP
cc27cb63 871 self._modify(modlist)
0d1fb712
MT
872
873 # Append to cache
874 self.ssh_keys.append(k)
875
876 def delete_ssh_key(self, key):
877 if not key in (k.keydata for k in self.ssh_keys):
878 return
879
880 # Delete key from LDAP
cc27cb63
MT
881 if len(self.ssh_keys) > 1:
882 self._delete_string("sshPublicKey", key)
883 else:
884 self._modify([
885 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
886 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
887 ])
0d1fb712 888
55b67ca4 889
940227cb
MT
890if __name__ == "__main__":
891 a = Accounts()
892
11347e46 893 print(a.list())