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