]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
Fix dimensions of Apple touch icons
[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
1babcd04 393 self.accounts._authenticate()
6b582a4f 394 self.ldap.passwd_s(self.dn, None, password)
3ea97943 395
940227cb
MT
396 def check_password(self, password):
397 """
398 Bind to the server with given credentials and return
399 true if password is corrent and false if not.
400
401 Raises exceptions from the server on any other errors.
402 """
0d1fb712
MT
403 if not password:
404 return
405
940227cb 406 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
407
408 # Create a new LDAP connection
409 ldap_uri = self.backend.settings.get("ldap_uri")
410 conn = ldap.initialize(ldap_uri)
411
940227cb 412 try:
3ea97943 413 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 414 except ldap.INVALID_CREDENTIALS:
3ea97943 415 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
416 return False
417
3ea97943
MT
418 logging.info("Successfully authenticated %s" % self)
419
940227cb
MT
420 return True
421
6b582a4f
MT
422 def check_password_quality(self, password):
423 """
424 Passwords are passed through zxcvbn to make sure
425 that they are strong enough.
426 """
427 return zxcvbn.zxcvbn(password, user_inputs=(
428 self.first_name, self.last_name,
429 ))
430
940227cb 431 def is_admin(self):
d82bc8e3 432 return "wheel" in self.groups
66862195 433
71a3109c
MT
434 def is_staff(self):
435 return "staff" in self.groups
436
437 def has_shell(self):
438 return "posixAccount" in self.classes
439
440 def has_mail(self):
441 return "postfixMailUser" in self.classes
442
443 def has_sip(self):
444 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
66862195 445
e96e445b
MT
446 def can_be_managed_by(self, account):
447 """
448 Returns True if account is allowed to manage this account
449 """
450 # Admins can manage all accounts
451 if account.is_admin():
452 return True
453
454 # Users can manage themselves
455 return self == account
456
66862195
MT
457 @property
458 def classes(self):
e96e445b 459 return self._get_strings("objectClass")
66862195
MT
460
461 @property
462 def uid(self):
e96e445b 463 return self._get_string("uid")
940227cb 464
a6dc0bad
MT
465 @property
466 def name(self):
e96e445b 467 return self._get_string("cn")
66862195 468
e96e445b
MT
469 # First Name
470
471 def get_first_name(self):
472 return self._get_string("givenName")
473
474 def set_first_name(self, first_name):
475 self._set_string("givenName", first_name)
476
477 # Update Common Name
478 self._set_string("cn", "%s %s" % (first_name, self.last_name))
479
480 first_name = property(get_first_name, set_first_name)
481
482 # Last Name
483
484 def get_last_name(self):
485 return self._get_string("sn")
486
487 def set_last_name(self, last_name):
488 self._set_string("sn", last_name)
489
490 # Update Common Name
491 self._set_string("cn", "%s %s" % (self.first_name, last_name))
492
493 last_name = property(get_last_name, set_last_name)
66862195 494
1bae74c7 495 @lazy_property
66862195 496 def groups(self):
819daf36
MT
497 groups = self.memcache.get("accounts:%s:groups" % self.dn)
498 if groups:
499 return groups
500
501 # Fetch groups from LDAP
502 groups = self._get_groups()
503
504 # Cache groups for 5 min
505 self.memcache.set("accounts:%s:groups" % self.dn, groups, 300)
506
507 return groups
508
509 def _get_groups(self):
1bae74c7 510 groups = []
66862195 511
1bae74c7
MT
512 res = self.accounts._query("(&(objectClass=posixGroup) \
513 (memberUid=%s))" % self.uid, ["cn"])
66862195 514
1bae74c7
MT
515 for dn, attrs in res:
516 cns = attrs.get("cn")
517 if cns:
518 groups.append(cns[0].decode())
66862195 519
1bae74c7 520 return groups
66862195 521
e96e445b
MT
522 # Address
523
0099c2a7
MT
524 @property
525 def address(self):
526 address = []
527
528 if self.street:
529 address += self.street.splitlines()
530
531 if self.postal_code and self.city:
532 if self.country_code in ("AT", "DE"):
533 address.append("%s %s" % (self.postal_code, self.city))
534 else:
535 address.append("%s, %s" % (self.city, self.postal_code))
536 else:
537 address.append(self.city or self.postal_code)
538
539 if self.country_name:
540 address.append(self.country_name)
541
542 return address
543
544 def get_street(self):
545 return self._get_string("street") or self._get_string("homePostalAddress")
546
547 def set_street(self, street):
548 self._set_string("street", street)
e96e445b 549
0099c2a7 550 street = property(get_street, set_street)
66862195 551
0099c2a7
MT
552 def get_city(self):
553 return self._get_string("l") or ""
e96e445b 554
0099c2a7
MT
555 def set_city(self, city):
556 self._set_string("l", city)
e96e445b 557
0099c2a7 558 city = property(get_city, set_city)
e96e445b 559
0099c2a7
MT
560 def get_postal_code(self):
561 return self._get_string("postalCode") or ""
562
563 def set_postal_code(self, postal_code):
564 self._set_string("postalCode", postal_code)
565
566 postal_code = property(get_postal_code, set_postal_code)
567
568 # XXX This should be c
569 def get_country_code(self):
570 return self._get_string("st")
571
572 def set_country_code(self, country_code):
573 self._set_string("st", country_code)
574
575 country_code = property(get_country_code, set_country_code)
576
577 @property
578 def country_name(self):
579 if self.country_code:
580 return countries.get_name(self.country_code)
a6dc0bad 581
940227cb
MT
582 @property
583 def email(self):
d86f6f18 584 return self._get_string("mail")
940227cb 585
e96e445b
MT
586 # Mail Routing Address
587
588 def get_mail_routing_address(self):
589 return self._get_string("mailRoutingAddress", None)
590
591 def set_mail_routing_address(self, address):
47bb098f 592 self._set_string("mailRoutingAddress", address or None)
e96e445b
MT
593
594 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
595
66862195
MT
596 @property
597 def sip_id(self):
598 if "sipUser" in self.classes:
e96e445b 599 return self._get_string("sipAuthenticationUser")
66862195
MT
600
601 if "sipRoutingObject" in self.classes:
e96e445b 602 return self._get_string("sipLocalAddress")
66862195 603
2f51147a
MT
604 @property
605 def sip_password(self):
e96e445b
MT
606 return self._get_string("sipPassword")
607
608 @staticmethod
609 def _generate_sip_password():
610 return util.random_string(8)
2f51147a 611
66862195
MT
612 @property
613 def sip_url(self):
614 return "%s@ipfire.org" % self.sip_id
615
616 def uses_sip_forwarding(self):
e96e445b 617 if self.sip_routing_address:
66862195
MT
618 return True
619
620 return False
621
e96e445b
MT
622 # SIP Routing
623
624 def get_sip_routing_address(self):
66862195 625 if "sipRoutingObject" in self.classes:
e96e445b
MT
626 return self._get_string("sipRoutingAddress")
627
628 def set_sip_routing_address(self, address):
629 if not address:
630 address = None
631
632 # Don't do anything if nothing has changed
633 if self.get_sip_routing_address() == address:
634 return
635
636 if address:
79cce555
MT
637 # This is no longer a SIP user any more
638 try:
639 self._modify([
640 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
641 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
642 (ldap.MOD_DELETE, "sipPassword", None),
643 ])
644 except ldap.NO_SUCH_ATTRIBUTE:
645 pass
646
647 # Set new routing object
648 try:
649 self._modify([
650 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
651 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
652 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
653 ])
654
655 # If this is a change, we cannot add this again
656 except ldap.TYPE_OR_VALUE_EXISTS:
657 self._set_string("sipRoutingAddress", address)
e96e445b 658 else:
79cce555
MT
659 try:
660 self._modify([
661 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
662 (ldap.MOD_DELETE, "sipLocalAddress", None),
663 (ldap.MOD_DELETE, "sipRoutingAddress", None),
664 ])
665 except ldap.NO_SUCH_ATTRIBUTE:
666 pass
667
668 self._modify([
e96e445b
MT
669 (ldap.MOD_ADD, "objectClass", b"sipUser"),
670 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
671 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
79cce555 672 ])
e96e445b
MT
673
674 # XXX Cache is invalid here
675
676 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 677
917434b8
MT
678 @lazy_property
679 def sip_registrations(self):
680 sip_registrations = []
681
682 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
683 reg.account = self
684
685 sip_registrations.append(reg)
686
687 return sip_registrations
688
1f38be5a
MT
689 @lazy_property
690 def sip_channels(self):
691 return self.backend.talk.freeswitch.get_sip_channels(self)
692
bdaf6b46
MT
693 def get_cdr(self, date=None, limit=None):
694 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 695
e96e445b 696 # Phone Numbers
6ff61434 697
d3208ac7
MT
698 @lazy_property
699 def phone_number(self):
700 """
701 Returns the IPFire phone number
702 """
703 if self.sip_id:
704 return phonenumbers.parse("+4923636035%s" % self.sip_id)
705
706 @lazy_property
707 def fax_number(self):
708 if self.sip_id:
709 return phonenumbers.parse("+49236360359%s" % self.sip_id)
710
e96e445b
MT
711 def get_phone_numbers(self):
712 ret = []
6ff61434 713
e96e445b
MT
714 for field in ("telephoneNumber", "homePhone", "mobile"):
715 for number in self._get_phone_numbers(field):
716 ret.append(number)
6ff61434 717
e96e445b
MT
718 return ret
719
720 def set_phone_numbers(self, phone_numbers):
721 # Sort phone numbers by landline and mobile
722 _landline_numbers = []
723 _mobile_numbers = []
724
725 for number in phone_numbers:
726 try:
727 number = phonenumbers.parse(number, None)
728 except phonenumbers.phonenumberutil.NumberParseException:
729 continue
730
731 # Convert to string (in E.164 format)
732 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
733
734 # Separate mobile numbers
735 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
736 _mobile_numbers.append(s)
737 else:
738 _landline_numbers.append(s)
739
740 # Save
741 self._set_strings("telephoneNumber", _landline_numbers)
742 self._set_strings("mobile", _mobile_numbers)
743
744 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
745
746 @property
747 def _all_telephone_numbers(self):
6ccc8acb
MT
748 ret = [ self.sip_id, ]
749
d3208ac7
MT
750 if self.phone_number:
751 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
752 ret.append(s)
753
6ccc8acb
MT
754 for number in self.phone_numbers:
755 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
756 ret.append(s)
757
758 return ret
66862195 759
2cd9af74
MT
760 def avatar_url(self, size=None):
761 if self.backend.debug:
03706893 762 hostname = "http://people.dev.ipfire.org"
2cd9af74 763 else:
03706893 764 hostname = "https://people.ipfire.org"
2cd9af74 765
03706893 766 url = "%s/users/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
767
768 if size:
769 url += "?size=%s" % size
770
771 return url
772
2cd9af74 773 def get_avatar(self, size=None):
5ef115cd 774 photo = self._get_bytes("jpegPhoto")
2cd9af74 775
0109451c
MT
776 # Exit if no avatar is available
777 if not photo:
778 return
779
5ef115cd
MT
780 # Return the raw image if no size was requested
781 if size is None:
782 return photo
2cd9af74 783
5ef115cd 784 # Try to retrieve something from the cache
9c01e5ac 785 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
5ef115cd
MT
786 if avatar:
787 return avatar
1a226c83 788
5ef115cd 789 # Generate a new thumbnail
2de3dacc 790 avatar = util.generate_thumbnail(photo, size, square=True)
1a226c83 791
5ef115cd 792 # Save to cache for 15m
9c01e5ac 793 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
1a226c83 794
5ef115cd 795 return avatar
2cd9af74 796
5cc10421
MT
797 def upload_avatar(self, avatar):
798 self._set("jpegPhoto", avatar)
799
f4672785
MT
800 # SSH Keys
801
802 @lazy_property
803 def ssh_keys(self):
804 ret = []
805
806 for key in self._get_strings("sshPublicKey"):
807 s = sshpubkeys.SSHKey()
808
809 try:
810 s.parse(key)
811 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
812 logging.warning("Could not parse SSH key %s: %s" % (key, e))
813 continue
814
815 ret.append(s)
816
817 return ret
60024cc8 818
44b75370 819 def get_ssh_key_by_hash_sha256(self, hash_sha256):
55b67ca4 820 for key in self.ssh_keys:
44b75370 821 if not key.hash_sha256() == hash_sha256:
55b67ca4
MT
822 continue
823
824 return key
825
0d1fb712
MT
826 def add_ssh_key(self, key):
827 k = sshpubkeys.SSHKey()
828
829 # Try to parse the key
830 k.parse(key)
831
832 # Check for types and sufficient sizes
833 if k.key_type == b"ssh-rsa":
834 if k.bits < 4096:
835 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
836
837 elif k.key_type == b"ssh-dss":
838 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
839
840 # Ignore any duplicates
841 if key in (k.keydata for k in self.ssh_keys):
842 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
843 return
844
cc27cb63
MT
845 # Prepare transaction
846 modlist = []
847
848 # Add object class if user is not in it, yet
849 if not "ldapPublicKey" in self.classes:
850 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
851
852 # Add key
853 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
854
0d1fb712 855 # Save key to LDAP
cc27cb63 856 self._modify(modlist)
0d1fb712
MT
857
858 # Append to cache
859 self.ssh_keys.append(k)
860
861 def delete_ssh_key(self, key):
862 if not key in (k.keydata for k in self.ssh_keys):
863 return
864
865 # Delete key from LDAP
cc27cb63
MT
866 if len(self.ssh_keys) > 1:
867 self._delete_string("sshPublicKey", key)
868 else:
869 self._modify([
870 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
871 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
872 ])
0d1fb712 873
55b67ca4 874
940227cb
MT
875if __name__ == "__main__":
876 a = Accounts()
877
11347e46 878 print(a.list())