]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
Revert "auth: Wrap login and logout in a single transaction"
[ipfire.org.git] / src / backend / accounts.py
1 #!/usr/bin/python
2 # encoding: utf-8
3
4 import PIL
5 import PIL.ImageOps
6 import datetime
7 import io
8 import ldap
9 import ldap.modlist
10 import logging
11 import phonenumbers
12 import sshpubkeys
13 import time
14 import urllib.parse
15 import urllib.request
16 import zxcvbn
17
18 from . import countries
19 from . import util
20 from .decorators import *
21 from .misc import Object
22
23 class Accounts(Object):
24 def init(self):
25 self.search_base = self.settings.get("ldap_search_base")
26
27 def __iter__(self):
28 # Only return developers (group with ID 1000)
29 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
30
31 return iter(sorted(accounts))
32
33 @lazy_property
34 def ldap(self):
35 # Connect to LDAP server
36 ldap_uri = self.settings.get("ldap_uri")
37
38 logging.debug("Connecting to LDAP server: %s" % ldap_uri)
39
40 # Connect to the LDAP server
41 conn = ldap.ldapobject.ReconnectLDAPObject(ldap_uri,
42 retry_max=10, retry_delay=3)
43
44 # Bind with username and password
45 bind_dn = self.settings.get("ldap_bind_dn")
46 if bind_dn:
47 bind_pw = self.settings.get("ldap_bind_pw", "")
48 conn.simple_bind(bind_dn, bind_pw)
49
50 return conn
51
52 def _query(self, query, attrlist=None, limit=0, search_base=None, scope=None):
53 logging.debug("Performing LDAP query: %s" % query)
54
55 t = time.time()
56
57 results = self.ldap.search_ext_s(search_base or self.search_base,
58 scope or ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit)
59
60 # Log time it took to perform the query
61 logging.debug("Query took %.2fms" % ((time.time() - t) * 1000.0))
62
63 return results
64
65 def _search(self, query, attrlist=None, limit=0):
66 dns = self._search_dns(query, limit=limit)
67
68 accounts = self._get_accounts_from_dns(dns)
69
70 return list(accounts)
71
72 def _search_dns(self, query, limit=0):
73 # Query cache
74 dns = self.memcache.get("accounts:search:%s" % query)
75 if dns is None:
76 # Return DNs only for this search query
77 dns = []
78 for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
79 dns.append(dn)
80
81 # Cache for 10 min
82 self.memcache.set("accounts:search:%s" % query, dns, 600)
83
84 return dns
85
86 def _get_accounts_from_dns(self, dns):
87 for dn in dns:
88 yield self.get_by_dn(dn)
89
90 def get_by_dn(self, dn):
91 attrs = self.memcache.get("accounts:%s:attrs" % dn)
92 if attrs is None:
93 attrs = self._get_attrs(dn)
94 assert attrs, dn
95
96 # Cache all attributes for 5 min
97 self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
98
99 return Account(self.backend, dn, attrs)
100
101 def _get_attrs(self, dn):
102 """
103 Fetches all attributes for the given distinguished name
104 """
105 results = self._query("(objectClass=*)", search_base=dn, limit=1)
106
107 for dn, attrs in results:
108 return attrs
109
110 def search(self, query):
111 # Search for exact matches
112 accounts = self._search(
113 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
114 % (query, query, query, query, query, query))
115
116 # Find accounts by name
117 if not accounts:
118 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
119 if not account in accounts:
120 accounts.append(account)
121
122 return sorted(accounts)
123
124 def _search_one(self, query):
125 results = self._search(query, limit=1)
126
127 for result in results:
128 return result
129
130 def uid_exists(self, uid):
131 if self.get_by_uid(uid):
132 return True
133
134 res = self.db.get("SELECT 1 FROM account_activations \
135 WHERE uid = %s AND expires_at > NOW()", uid)
136
137 if res:
138 return True
139
140 # Account with uid does not exist, yet
141 return False
142
143 def get_by_uid(self, uid):
144 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
145
146 def get_by_mail(self, mail):
147 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
148
149 def find_account(self, s):
150 account = self.get_by_uid(s)
151 if account:
152 return account
153
154 return self.get_by_mail(s)
155
156 def get_by_sip_id(self, sip_id):
157 if not sip_id:
158 return
159
160 return self._search_one(
161 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
162 % (sip_id, sip_id))
163
164 def get_by_phone_number(self, number):
165 if not number:
166 return
167
168 return self._search_one(
169 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
170 % (number, number, number, number))
171
172 # Registration
173
174 def register(self, uid, email, first_name, last_name):
175 # Check if UID is unique
176 if self.get_by_uid(uid):
177 raise ValueError("UID exists: %s" % uid)
178
179 # Generate a random activation code
180 activation_code = util.random_string(36)
181
182 # Create an entry in our database until the user
183 # has activated the account
184 self.db.execute("INSERT INTO account_activations(uid, activation_code, \
185 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
186 uid, activation_code, email, first_name, last_name)
187
188 # Send an account activation email
189 self.backend.messages.send_template("auth/messages/register",
190 recipients=[email], priority=100, uid=uid,
191 activation_code=activation_code, email=email,
192 first_name=first_name, last_name=last_name)
193
194 def activate(self, uid, activation_code):
195 res = self.db.get("DELETE FROM account_activations \
196 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
197 RETURNING *", uid, activation_code)
198
199 # Return nothing when account was not found
200 if not res:
201 return
202
203 # Create a new account on the LDAP database
204 return self.create(uid, res.email,
205 first_name=res.first_name, last_name=res.last_name)
206
207 def create(self, uid, email, first_name, last_name):
208 # Account Parameters
209 account = {
210 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
211 "mail" : email.encode(),
212
213 # Name
214 "cn" : b"%s %s" % (first_name.encode(), last_name.encode()),
215 "sn" : last_name.encode(),
216 "givenName" : first_name.encode(),
217 }
218
219 logging.info("Creating new account: %s: %s" % (uid, account))
220
221 # Create DN
222 dn = "uid=%s,ou=People,dc=mcfly,dc=local" % uid
223
224 # Create account on LDAP
225 self.ldap.add_s(dn, ldap.modlist.addModlist(account))
226
227 # Return account
228 return self.get_by_dn(dn)
229
230 # Session stuff
231
232 def _cleanup_expired_sessions(self):
233 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
234
235 def create_session(self, account, host):
236 self._cleanup_expired_sessions()
237
238 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
239 RETURNING session_id, time_expires", host, account.uid)
240
241 # Session could not be created
242 if not res:
243 return None, None
244
245 logging.info("Created session %s for %s which expires %s" \
246 % (res.session_id, account, res.time_expires))
247 return res.session_id, res.time_expires
248
249 def destroy_session(self, session_id, host):
250 logging.info("Destroying session %s" % session_id)
251
252 self.db.execute("DELETE FROM sessions \
253 WHERE session_id = %s AND host = %s", session_id, host)
254 self._cleanup_expired_sessions()
255
256 def get_by_session(self, session_id, host):
257 logging.debug("Looking up session %s" % session_id)
258
259 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
260 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
261 session_id, host)
262
263 # Session does not exist or has expired
264 if not res:
265 return
266
267 # Update the session expiration time
268 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
269 WHERE session_id = %s AND host = %s", session_id, host)
270
271 return self.get_by_uid(res.uid)
272
273
274 class Account(Object):
275 def __init__(self, backend, dn, attrs=None):
276 Object.__init__(self, backend)
277 self.dn = dn
278
279 self.attributes = attrs or {}
280
281 def __str__(self):
282 return self.name
283
284 def __repr__(self):
285 return "<%s %s>" % (self.__class__.__name__, self.dn)
286
287 def __eq__(self, other):
288 if isinstance(other, self.__class__):
289 return self.dn == other.dn
290
291 def __lt__(self, other):
292 if isinstance(other, self.__class__):
293 return self.name < other.name
294
295 @property
296 def ldap(self):
297 return self.accounts.ldap
298
299 def _exists(self, key):
300 try:
301 self.attributes[key]
302 except KeyError:
303 return False
304
305 return True
306
307 def _get(self, key):
308 for value in self.attributes.get(key, []):
309 yield value
310
311 def _get_bytes(self, key, default=None):
312 for value in self._get(key):
313 return value
314
315 return default
316
317 def _get_strings(self, key):
318 for value in self._get(key):
319 yield value.decode()
320
321 def _get_string(self, key, default=None):
322 for value in self._get_strings(key):
323 return value
324
325 return default
326
327 def _get_phone_numbers(self, key):
328 for value in self._get_strings(key):
329 yield phonenumbers.parse(value, None)
330
331 def _modify(self, modlist):
332 logging.debug("Modifying %s: %s" % (self.dn, modlist))
333
334 # Run modify operation
335 self.ldap.modify_s(self.dn, modlist)
336
337 # Delete cached attributes
338 self.memcache.delete("accounts:%s:attrs")
339
340 def _set(self, key, values):
341 current = self._get(key)
342
343 # Don't do anything if nothing has changed
344 if list(current) == values:
345 return
346
347 # Remove all old values and add all new ones
348 modlist = []
349
350 if self._exists(key):
351 modlist.append((ldap.MOD_DELETE, key, None))
352
353 # Add new values
354 if values:
355 modlist.append((ldap.MOD_ADD, key, values))
356
357 # Run modify operation
358 self._modify(modlist)
359
360 # Update cache
361 self.attributes.update({ key : values })
362
363 def _set_bytes(self, key, values):
364 return self._set(key, values)
365
366 def _set_strings(self, key, values):
367 return self._set(key, [e.encode() for e in values if e])
368
369 def _set_string(self, key, value):
370 return self._set_strings(key, [value,])
371
372 def _add(self, key, values):
373 modlist = [
374 (ldap.MOD_ADD, key, values),
375 ]
376
377 self._modify(modlist)
378
379 def _add_strings(self, key, values):
380 return self._add(key, [e.encode() for e in values])
381
382 def _add_string(self, key, value):
383 return self._add_strings(key, [value,])
384
385 def _delete(self, key, values):
386 modlist = [
387 (ldap.MOD_DELETE, key, values),
388 ]
389
390 self._modify(modlist)
391
392 def _delete_strings(self, key, values):
393 return self._delete(key, [e.encode() for e in values])
394
395 def _delete_string(self, key, value):
396 return self._delete_strings(key, [value,])
397
398 def passwd(self, password):
399 """
400 Sets a new password
401 """
402 # The new password must have a score of 3 or better
403 quality = self.check_password_quality(password)
404 if quality["score"] < 3:
405 raise ValueError("Password too weak")
406
407 self.ldap.passwd_s(self.dn, None, password)
408
409 def check_password(self, password):
410 """
411 Bind to the server with given credentials and return
412 true if password is corrent and false if not.
413
414 Raises exceptions from the server on any other errors.
415 """
416 if not password:
417 return
418
419 logging.debug("Checking credentials for %s" % self.dn)
420
421 # Create a new LDAP connection
422 ldap_uri = self.backend.settings.get("ldap_uri")
423 conn = ldap.initialize(ldap_uri)
424
425 try:
426 conn.simple_bind_s(self.dn, password.encode("utf-8"))
427 except ldap.INVALID_CREDENTIALS:
428 logging.debug("Account credentials are invalid for %s" % self)
429 return False
430
431 logging.info("Successfully authenticated %s" % self)
432
433 return True
434
435 def check_password_quality(self, password):
436 """
437 Passwords are passed through zxcvbn to make sure
438 that they are strong enough.
439 """
440 return zxcvbn.zxcvbn(password, user_inputs=(
441 self.first_name, self.last_name,
442 ))
443
444 def is_admin(self):
445 return "wheel" in self.groups
446
447 def is_staff(self):
448 return "staff" in self.groups
449
450 def has_shell(self):
451 return "posixAccount" in self.classes
452
453 def has_mail(self):
454 return "postfixMailUser" in self.classes
455
456 def has_sip(self):
457 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
458
459 def can_be_managed_by(self, account):
460 """
461 Returns True if account is allowed to manage this account
462 """
463 # Admins can manage all accounts
464 if account.is_admin():
465 return True
466
467 # Users can manage themselves
468 return self == account
469
470 @property
471 def classes(self):
472 return self._get_strings("objectClass")
473
474 @property
475 def uid(self):
476 return self._get_string("uid")
477
478 @property
479 def name(self):
480 return self._get_string("cn")
481
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)
507
508 @lazy_property
509 def groups(self):
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):
523 groups = []
524
525 res = self.accounts._query("(&(objectClass=posixGroup) \
526 (memberUid=%s))" % self.uid, ["cn"])
527
528 for dn, attrs in res:
529 cns = attrs.get("cn")
530 if cns:
531 groups.append(cns[0].decode())
532
533 return groups
534
535 # Address
536
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)
562
563 street = property(get_street, set_street)
564
565 def get_city(self):
566 return self._get_string("l") or ""
567
568 def set_city(self, city):
569 self._set_string("l", city)
570
571 city = property(get_city, set_city)
572
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)
594
595 @property
596 def email(self):
597 return self._get_string("mail")
598
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):
605 self._set_string("mailRoutingAddress", address or None)
606
607 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
608
609 @property
610 def sip_id(self):
611 if "sipUser" in self.classes:
612 return self._get_string("sipAuthenticationUser")
613
614 if "sipRoutingObject" in self.classes:
615 return self._get_string("sipLocalAddress")
616
617 @property
618 def sip_password(self):
619 return self._get_string("sipPassword")
620
621 @staticmethod
622 def _generate_sip_password():
623 return util.random_string(8)
624
625 @property
626 def sip_url(self):
627 return "%s@ipfire.org" % self.sip_id
628
629 def uses_sip_forwarding(self):
630 if self.sip_routing_address:
631 return True
632
633 return False
634
635 # SIP Routing
636
637 def get_sip_routing_address(self):
638 if "sipRoutingObject" in self.classes:
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:
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)
671 else:
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([
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()),
685 ])
686
687 # XXX Cache is invalid here
688
689 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
690
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
702 @lazy_property
703 def sip_channels(self):
704 return self.backend.talk.freeswitch.get_sip_channels(self)
705
706 def get_cdr(self, date=None, limit=None):
707 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
708
709 # Phone Numbers
710
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
724 def get_phone_numbers(self):
725 ret = []
726
727 for field in ("telephoneNumber", "homePhone", "mobile"):
728 for number in self._get_phone_numbers(field):
729 ret.append(number)
730
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)
758
759 @property
760 def _all_telephone_numbers(self):
761 ret = [ self.sip_id, ]
762
763 if self.phone_number:
764 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
765 ret.append(s)
766
767 for number in self.phone_numbers:
768 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
769 ret.append(s)
770
771 return ret
772
773 def avatar_url(self, size=None):
774 if self.backend.debug:
775 hostname = "http://people.dev.ipfire.org"
776 else:
777 hostname = "https://people.ipfire.org"
778
779 url = "%s/users/%s.jpg" % (hostname, self.uid)
780
781 if size:
782 url += "?size=%s" % size
783
784 return url
785
786 def get_avatar(self, size=None):
787 avatar = self._get_bytes("jpegPhoto")
788 if not avatar:
789 return
790
791 if not size:
792 return avatar
793
794 return self._resize_avatar(avatar, size)
795
796 def _resize_avatar(self, image, size):
797 image = PIL.Image.open(io.BytesIO(image))
798
799 # Resize the image to the desired resolution (and make it square)
800 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
801
802 with io.BytesIO() as f:
803 # If writing out the image does not work with optimization,
804 # we try to write it out without any optimization.
805 try:
806 thumbnail.save(f, image.format, optimize=True, quality=98)
807 except:
808 thumbnail.save(f, image.format, quality=98)
809
810 return f.getvalue()
811
812 def upload_avatar(self, avatar):
813 self._set("jpegPhoto", avatar)
814
815 # SSH Keys
816
817 @lazy_property
818 def ssh_keys(self):
819 ret = []
820
821 for key in self._get_strings("sshPublicKey"):
822 s = sshpubkeys.SSHKey()
823
824 try:
825 s.parse(key)
826 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
827 logging.warning("Could not parse SSH key %s: %s" % (key, e))
828 continue
829
830 ret.append(s)
831
832 return ret
833
834 def get_ssh_key_by_hash_sha256(self, hash_sha256):
835 for key in self.ssh_keys:
836 if not key.hash_sha256() == hash_sha256:
837 continue
838
839 return key
840
841 def add_ssh_key(self, key):
842 k = sshpubkeys.SSHKey()
843
844 # Try to parse the key
845 k.parse(key)
846
847 # Check for types and sufficient sizes
848 if k.key_type == b"ssh-rsa":
849 if k.bits < 4096:
850 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
851
852 elif k.key_type == b"ssh-dss":
853 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
854
855 # Ignore any duplicates
856 if key in (k.keydata for k in self.ssh_keys):
857 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
858 return
859
860 # Prepare transaction
861 modlist = []
862
863 # Add object class if user is not in it, yet
864 if not "ldapPublicKey" in self.classes:
865 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
866
867 # Add key
868 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
869
870 # Save key to LDAP
871 self._modify(modlist)
872
873 # Append to cache
874 self.ssh_keys.append(k)
875
876 def delete_ssh_key(self, key):
877 if not key in (k.keydata for k in self.ssh_keys):
878 return
879
880 # Delete key from LDAP
881 if len(self.ssh_keys) > 1:
882 self._delete_string("sshPublicKey", key)
883 else:
884 self._modify([
885 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
886 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
887 ])
888
889
890 if __name__ == "__main__":
891 a = Accounts()
892
893 print(a.list())