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