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