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