]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
accounts: Remove search cache
[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):
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 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 accounts = []
67 for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
68 account = self.get_by_dn(dn)
69 accounts.append(account)
70
71 return accounts
72
73 def _get_attrs(self, dn):
74 """
75 Fetches all attributes for the given distinguished name
76 """
77 results = self._query("(objectClass=*)", search_base=dn, limit=1)
78
79 for dn, attrs in results:
80 return attrs
81
82 def get_by_dn(self, dn):
83 attrs = self.memcache.get("accounts:%s:attrs" % dn)
84 if attrs is None:
85 attrs = self._get_attrs(dn)
86 assert attrs, dn
87
88 # Cache all attributes for 5 min
89 self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
90
91 return Account(self.backend, dn, attrs)
92
93 def search(self, query):
94 # Search for exact matches
95 accounts = self._search(
96 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
97 % (query, query, query, query, query, query))
98
99 # Find accounts by name
100 if not accounts:
101 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
102 if not account in accounts:
103 accounts.append(account)
104
105 return sorted(accounts)
106
107 def _search_one(self, query):
108 results = self._search(query, limit=1)
109
110 for result in results:
111 return result
112
113 def uid_exists(self, uid):
114 if self.get_by_uid(uid):
115 return True
116
117 res = self.db.get("SELECT 1 FROM account_activations \
118 WHERE uid = %s AND expires_at > NOW()", uid)
119
120 if res:
121 return True
122
123 # Account with uid does not exist, yet
124 return False
125
126 def get_by_uid(self, uid):
127 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
128
129 def get_by_mail(self, mail):
130 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
131
132 def find_account(self, s):
133 account = self.get_by_uid(s)
134 if account:
135 return account
136
137 return self.get_by_mail(s)
138
139 def get_by_sip_id(self, sip_id):
140 if not sip_id:
141 return
142
143 return self._search_one(
144 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
145 % (sip_id, sip_id))
146
147 def get_by_phone_number(self, number):
148 if not number:
149 return
150
151 return self._search_one(
152 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
153 % (number, number, number, number))
154
155 # Registration
156
157 def register(self, uid, email, first_name, last_name):
158 # Check if UID is unique
159 if self.get_by_uid(uid):
160 raise ValueError("UID exists: %s" % uid)
161
162 # Generate a random activation code
163 activation_code = util.random_string(36)
164
165 # Create an entry in our database until the user
166 # has activated the account
167 self.db.execute("INSERT INTO account_activations(uid, activation_code, \
168 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
169 uid, activation_code, email, first_name, last_name)
170
171 # Send an account activation email
172 self.backend.messages.send_template("auth/messages/register",
173 recipients=[email], priority=100, uid=uid,
174 activation_code=activation_code, email=email,
175 first_name=first_name, last_name=last_name)
176
177 def activate(self, uid, activation_code):
178 res = self.db.get("DELETE FROM account_activations \
179 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
180 RETURNING *", uid, activation_code)
181
182 # Return nothing when account was not found
183 if not res:
184 return
185
186 # Create a new account on the LDAP database
187 return self.create(uid, res.email,
188 first_name=res.first_name, last_name=res.last_name)
189
190 def create(self, uid, email, first_name, last_name):
191 # Account Parameters
192 account = {
193 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
194 "mail" : email.encode(),
195
196 # Name
197 "cn" : b"%s %s" % (first_name.encode(), last_name.encode()),
198 "sn" : last_name.encode(),
199 "givenName" : first_name.encode(),
200 }
201
202 logging.info("Creating new account: %s: %s" % (uid, account))
203
204 # Create DN
205 dn = "uid=%s,ou=People,dc=mcfly,dc=local" % uid
206
207 # Create account on LDAP
208 self.ldap.add_s(dn, ldap.modlist.addModlist(account))
209
210 # Return account
211 return self.get_by_dn(dn)
212
213 # Session stuff
214
215 def _cleanup_expired_sessions(self):
216 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
217
218 def create_session(self, account, host):
219 self._cleanup_expired_sessions()
220
221 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
222 RETURNING session_id, time_expires", host, account.uid)
223
224 # Session could not be created
225 if not res:
226 return None, None
227
228 logging.info("Created session %s for %s which expires %s" \
229 % (res.session_id, account, res.time_expires))
230 return res.session_id, res.time_expires
231
232 def destroy_session(self, session_id, host):
233 logging.info("Destroying session %s" % session_id)
234
235 self.db.execute("DELETE FROM sessions \
236 WHERE session_id = %s AND host = %s", session_id, host)
237 self._cleanup_expired_sessions()
238
239 def get_by_session(self, session_id, host):
240 logging.debug("Looking up session %s" % session_id)
241
242 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
243 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
244 session_id, host)
245
246 # Session does not exist or has expired
247 if not res:
248 return
249
250 # Update the session expiration time
251 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
252 WHERE session_id = %s AND host = %s", session_id, host)
253
254 return self.get_by_uid(res.uid)
255
256
257 class Account(Object):
258 def __init__(self, backend, dn, attrs=None):
259 Object.__init__(self, backend)
260 self.dn = dn
261
262 self.attributes = attrs or {}
263
264 def __str__(self):
265 return self.name
266
267 def __repr__(self):
268 return "<%s %s>" % (self.__class__.__name__, self.dn)
269
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
277
278 @property
279 def ldap(self):
280 return self.accounts.ldap
281
282 def _exists(self, key):
283 try:
284 self.attributes[key]
285 except KeyError:
286 return False
287
288 return True
289
290 def _get(self, key):
291 for value in self.attributes.get(key, []):
292 yield value
293
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
320 # Delete cached attributes
321 self.memcache.delete("accounts:%s:attrs")
322
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 = []
332
333 if self._exists(key):
334 modlist.append((ldap.MOD_DELETE, key, None))
335
336 # Add new values
337 if values:
338 modlist.append((ldap.MOD_ADD, key, values))
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):
350 return self._set(key, [e.encode() for e in values if e])
351
352 def _set_string(self, key, value):
353 return self._set_strings(key, [value,])
354
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
381 def passwd(self, password):
382 """
383 Sets a new password
384 """
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)
391
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 """
399 if not password:
400 return
401
402 logging.debug("Checking credentials for %s" % self.dn)
403
404 # Create a new LDAP connection
405 ldap_uri = self.backend.settings.get("ldap_uri")
406 conn = ldap.initialize(ldap_uri)
407
408 try:
409 conn.simple_bind_s(self.dn, password.encode("utf-8"))
410 except ldap.INVALID_CREDENTIALS:
411 logging.debug("Account credentials are invalid for %s" % self)
412 return False
413
414 logging.info("Successfully authenticated %s" % self)
415
416 return True
417
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
427 def is_admin(self):
428 return "wheel" in self.groups
429
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
441
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
453 @property
454 def classes(self):
455 return self._get_strings("objectClass")
456
457 @property
458 def uid(self):
459 return self._get_string("uid")
460
461 @property
462 def name(self):
463 return self._get_string("cn")
464
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)
490
491 @lazy_property
492 def groups(self):
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):
506 groups = []
507
508 res = self.accounts._query("(&(objectClass=posixGroup) \
509 (memberUid=%s))" % self.uid, ["cn"])
510
511 for dn, attrs in res:
512 cns = attrs.get("cn")
513 if cns:
514 groups.append(cns[0].decode())
515
516 return groups
517
518 # Address
519
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)
545
546 street = property(get_street, set_street)
547
548 def get_city(self):
549 return self._get_string("l") or ""
550
551 def set_city(self, city):
552 self._set_string("l", city)
553
554 city = property(get_city, set_city)
555
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)
577
578 @property
579 def email(self):
580 return self._get_string("mail")
581
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):
588 self._set_string("mailRoutingAddress", address or None)
589
590 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
591
592 @property
593 def sip_id(self):
594 if "sipUser" in self.classes:
595 return self._get_string("sipAuthenticationUser")
596
597 if "sipRoutingObject" in self.classes:
598 return self._get_string("sipLocalAddress")
599
600 @property
601 def sip_password(self):
602 return self._get_string("sipPassword")
603
604 @staticmethod
605 def _generate_sip_password():
606 return util.random_string(8)
607
608 @property
609 def sip_url(self):
610 return "%s@ipfire.org" % self.sip_id
611
612 def uses_sip_forwarding(self):
613 if self.sip_routing_address:
614 return True
615
616 return False
617
618 # SIP Routing
619
620 def get_sip_routing_address(self):
621 if "sipRoutingObject" in self.classes:
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:
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)
654 else:
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([
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()),
668 ])
669
670 # XXX Cache is invalid here
671
672 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
673
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
685 @lazy_property
686 def sip_channels(self):
687 return self.backend.talk.freeswitch.get_sip_channels(self)
688
689 def get_cdr(self, date=None, limit=None):
690 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
691
692 # Phone Numbers
693
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
707 def get_phone_numbers(self):
708 ret = []
709
710 for field in ("telephoneNumber", "homePhone", "mobile"):
711 for number in self._get_phone_numbers(field):
712 ret.append(number)
713
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)
741
742 @property
743 def _all_telephone_numbers(self):
744 ret = [ self.sip_id, ]
745
746 if self.phone_number:
747 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
748 ret.append(s)
749
750 for number in self.phone_numbers:
751 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
752 ret.append(s)
753
754 return ret
755
756 def avatar_url(self, size=None):
757 if self.backend.debug:
758 hostname = "http://people.dev.ipfire.org"
759 else:
760 hostname = "https://people.ipfire.org"
761
762 url = "%s/users/%s.jpg" % (hostname, self.uid)
763
764 if size:
765 url += "?size=%s" % size
766
767 return url
768
769 def get_avatar(self, size=None):
770 avatar = self._get_bytes("jpegPhoto")
771 if not avatar:
772 return
773
774 if not size:
775 return avatar
776
777 return self._resize_avatar(avatar, size)
778
779 def _resize_avatar(self, image, size):
780 image = PIL.Image.open(io.BytesIO(image))
781
782 # Resize the image to the desired resolution (and make it square)
783 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
784
785 with io.BytesIO() as f:
786 # If writing out the image does not work with optimization,
787 # we try to write it out without any optimization.
788 try:
789 thumbnail.save(f, image.format, optimize=True, quality=98)
790 except:
791 thumbnail.save(f, image.format, quality=98)
792
793 return f.getvalue()
794
795 def upload_avatar(self, avatar):
796 self._set("jpegPhoto", avatar)
797
798 # SSH Keys
799
800 @lazy_property
801 def ssh_keys(self):
802 ret = []
803
804 for key in self._get_strings("sshPublicKey"):
805 s = sshpubkeys.SSHKey()
806
807 try:
808 s.parse(key)
809 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
810 logging.warning("Could not parse SSH key %s: %s" % (key, e))
811 continue
812
813 ret.append(s)
814
815 return ret
816
817 def get_ssh_key_by_hash_sha256(self, hash_sha256):
818 for key in self.ssh_keys:
819 if not key.hash_sha256() == hash_sha256:
820 continue
821
822 return key
823
824 def add_ssh_key(self, key):
825 k = sshpubkeys.SSHKey()
826
827 # Try to parse the key
828 k.parse(key)
829
830 # Check for types and sufficient sizes
831 if k.key_type == b"ssh-rsa":
832 if k.bits < 4096:
833 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
834
835 elif k.key_type == b"ssh-dss":
836 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
837
838 # Ignore any duplicates
839 if key in (k.keydata for k in self.ssh_keys):
840 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
841 return
842
843 # Prepare transaction
844 modlist = []
845
846 # Add object class if user is not in it, yet
847 if not "ldapPublicKey" in self.classes:
848 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
849
850 # Add key
851 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
852
853 # Save key to LDAP
854 self._modify(modlist)
855
856 # Append to cache
857 self.ssh_keys.append(k)
858
859 def delete_ssh_key(self, key):
860 if not key in (k.keydata for k in self.ssh_keys):
861 return
862
863 # Delete key from LDAP
864 if len(self.ssh_keys) > 1:
865 self._delete_string("sshPublicKey", key)
866 else:
867 self._modify([
868 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
869 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
870 ])
871
872
873 if __name__ == "__main__":
874 a = Accounts()
875
876 print(a.list())