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