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