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