]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/accounts.py
1e586fc54eaf08423da7c9b923b2ba16abebd15e
[ipfire.org.git] / src / backend / accounts.py
1 #!/usr/bin/python
2 # encoding: utf-8
3
4 import datetime
5 import ldap
6 import ldap.modlist
7 import logging
8 import phonenumbers
9 import sshpubkeys
10 import time
11 import urllib.parse
12 import urllib.request
13 import zxcvbn
14
15 from . import countries
16 from . import util
17 from .decorators import *
18 from .misc import Object
19
20 class Accounts(Object):
21 def init(self):
22 self.search_base = self.settings.get("ldap_search_base")
23
24 def __iter__(self):
25 # Only return developers (group with ID 1000)
26 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
27
28 return iter(sorted(accounts))
29
30 @lazy_property
31 def ldap(self):
32 # Connect to LDAP server
33 ldap_uri = self.settings.get("ldap_uri")
34
35 logging.debug("Connecting to LDAP server: %s" % ldap_uri)
36
37 # Connect to the LDAP server
38 return ldap.ldapobject.ReconnectLDAPObject(ldap_uri,
39 retry_max=10, retry_delay=3)
40
41 def _authenticate(self):
42 # Bind with username and password
43 self.ldap.simple_bind(
44 self.settings.get("ldap_bind_dn"),
45 self.settings.get("ldap_bind_pw", ""),
46 )
47
48 def _query(self, query, attrlist=None, limit=0, search_base=None):
49 logging.debug("Performing LDAP query: %s" % query)
50
51 t = time.time()
52
53 results = self.ldap.search_ext_s(search_base or self.search_base,
54 ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit)
55
56 # Log time it took to perform the query
57 logging.debug("Query took %.2fms" % ((time.time() - t) * 1000.0))
58
59 return results
60
61 def _search(self, query, attrlist=None, limit=0):
62 accounts = []
63 for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
64 account = self.get_by_dn(dn)
65 accounts.append(account)
66
67 return accounts
68
69 def _get_attrs(self, dn):
70 """
71 Fetches all attributes for the given distinguished name
72 """
73 results = self._query("(objectClass=*)", search_base=dn, limit=1)
74
75 for dn, attrs in results:
76 return attrs
77
78 def get_by_dn(self, dn):
79 attrs = self.memcache.get("accounts:%s:attrs" % dn)
80 if attrs is None:
81 attrs = self._get_attrs(dn)
82 assert attrs, dn
83
84 # Cache all attributes for 5 min
85 self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
86
87 return Account(self.backend, dn, attrs)
88
89 def search(self, query):
90 # Search for exact matches
91 accounts = self._search(
92 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
93 % (query, query, query, query, query, query))
94
95 # Find accounts by name
96 if not accounts:
97 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
98 if not account in accounts:
99 accounts.append(account)
100
101 return sorted(accounts)
102
103 def _search_one(self, query):
104 results = self._search(query, limit=1)
105
106 for result in results:
107 return result
108
109 def uid_exists(self, uid):
110 if self.get_by_uid(uid):
111 return True
112
113 res = self.db.get("SELECT 1 FROM account_activations \
114 WHERE uid = %s AND expires_at > NOW()", uid)
115
116 if res:
117 return True
118
119 # Account with uid does not exist, yet
120 return False
121
122 def get_by_uid(self, uid):
123 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
124
125 def get_by_mail(self, mail):
126 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
127
128 def find_account(self, s):
129 account = self.get_by_uid(s)
130 if account:
131 return account
132
133 return self.get_by_mail(s)
134
135 def get_by_sip_id(self, sip_id):
136 if not sip_id:
137 return
138
139 return self._search_one(
140 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
141 % (sip_id, sip_id))
142
143 def get_by_phone_number(self, number):
144 if not number:
145 return
146
147 return self._search_one(
148 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
149 % (number, number, number, number))
150
151 # Registration
152
153 def register(self, uid, email, first_name, last_name):
154 # Check if UID is unique
155 if self.get_by_uid(uid):
156 raise ValueError("UID exists: %s" % uid)
157
158 # Generate a random activation code
159 activation_code = util.random_string(36)
160
161 # Create an entry in our database until the user
162 # has activated the account
163 self.db.execute("INSERT INTO account_activations(uid, activation_code, \
164 email, first_name, last_name) VALUES(%s, %s, %s, %s, %s)",
165 uid, activation_code, email, first_name, last_name)
166
167 # Send an account activation email
168 self.backend.messages.send_template("auth/messages/register",
169 recipients=[email], priority=100, uid=uid,
170 activation_code=activation_code, email=email,
171 first_name=first_name, last_name=last_name)
172
173 def activate(self, uid, activation_code):
174 res = self.db.get("DELETE FROM account_activations \
175 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
176 RETURNING *", uid, activation_code)
177
178 # Return nothing when account was not found
179 if not res:
180 return
181
182 # Create a new account on the LDAP database
183 return self.create(uid, res.email,
184 first_name=res.first_name, last_name=res.last_name)
185
186 def create(self, uid, email, first_name, last_name):
187 cn = "%s %s" % (first_name, last_name)
188
189 # Account Parameters
190 account = {
191 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
192 "mail" : email.encode(),
193
194 # Name
195 "cn" : cn.encode(),
196 "sn" : last_name.encode(),
197 "givenName" : first_name.encode(),
198 }
199
200 logging.info("Creating new account: %s: %s" % (uid, account))
201
202 # Create DN
203 dn = "uid=%s,ou=People,dc=mcfly,dc=local" % uid
204
205 # Create account on LDAP
206 self.accounts._authenticate()
207 self.ldap.add_s(dn, ldap.modlist.addModlist(account))
208
209 # Return account
210 return self.get_by_dn(dn)
211
212 # Session stuff
213
214 def create_session(self, account, host):
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)
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)
248
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
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 if self.nickname:
266 return self.nickname
267
268 return self.name
269
270 def __repr__(self):
271 return "<%s %s>" % (self.__class__.__name__, self.dn)
272
273 def __eq__(self, other):
274 if isinstance(other, self.__class__):
275 return self.dn == other.dn
276
277 def __lt__(self, other):
278 if isinstance(other, self.__class__):
279 return self.name < other.name
280
281 @property
282 def ldap(self):
283 return self.accounts.ldap
284
285 def _exists(self, key):
286 try:
287 self.attributes[key]
288 except KeyError:
289 return False
290
291 return True
292
293 def _get(self, key):
294 for value in self.attributes.get(key, []):
295 yield value
296
297 def _get_bytes(self, key, default=None):
298 for value in self._get(key):
299 return value
300
301 return default
302
303 def _get_strings(self, key):
304 for value in self._get(key):
305 yield value.decode()
306
307 def _get_string(self, key, default=None):
308 for value in self._get_strings(key):
309 return value
310
311 return default
312
313 def _get_phone_numbers(self, key):
314 for value in self._get_strings(key):
315 yield phonenumbers.parse(value, None)
316
317 def _modify(self, modlist):
318 logging.debug("Modifying %s: %s" % (self.dn, modlist))
319
320 # Authenticate before performing any write operations
321 self.accounts._authenticate()
322
323 # Run modify operation
324 self.ldap.modify_s(self.dn, modlist)
325
326 # Delete cached attributes
327 self.memcache.delete("accounts:%s:attrs" % self.dn)
328
329 def _set(self, key, values):
330 current = self._get(key)
331
332 # Don't do anything if nothing has changed
333 if list(current) == values:
334 return
335
336 # Remove all old values and add all new ones
337 modlist = []
338
339 if self._exists(key):
340 modlist.append((ldap.MOD_DELETE, key, None))
341
342 # Add new values
343 if values:
344 modlist.append((ldap.MOD_ADD, key, values))
345
346 # Run modify operation
347 self._modify(modlist)
348
349 # Update cache
350 self.attributes.update({ key : values })
351
352 def _set_bytes(self, key, values):
353 return self._set(key, values)
354
355 def _set_strings(self, key, values):
356 return self._set(key, [e.encode() for e in values if e])
357
358 def _set_string(self, key, value):
359 return self._set_strings(key, [value,])
360
361 def _add(self, key, values):
362 modlist = [
363 (ldap.MOD_ADD, key, values),
364 ]
365
366 self._modify(modlist)
367
368 def _add_strings(self, key, values):
369 return self._add(key, [e.encode() for e in values])
370
371 def _add_string(self, key, value):
372 return self._add_strings(key, [value,])
373
374 def _delete(self, key, values):
375 modlist = [
376 (ldap.MOD_DELETE, key, values),
377 ]
378
379 self._modify(modlist)
380
381 def _delete_strings(self, key, values):
382 return self._delete(key, [e.encode() for e in values])
383
384 def _delete_string(self, key, value):
385 return self._delete_strings(key, [value,])
386
387 def passwd(self, password):
388 """
389 Sets a new password
390 """
391 # The new password must have a score of 3 or better
392 quality = self.check_password_quality(password)
393 if quality["score"] < 3:
394 raise ValueError("Password too weak")
395
396 self.accounts._authenticate()
397 self.ldap.passwd_s(self.dn, None, password)
398
399 def check_password(self, password):
400 """
401 Bind to the server with given credentials and return
402 true if password is corrent and false if not.
403
404 Raises exceptions from the server on any other errors.
405 """
406 if not password:
407 return
408
409 logging.debug("Checking credentials for %s" % self.dn)
410
411 # Create a new LDAP connection
412 ldap_uri = self.backend.settings.get("ldap_uri")
413 conn = ldap.initialize(ldap_uri)
414
415 try:
416 conn.simple_bind_s(self.dn, password.encode("utf-8"))
417 except ldap.INVALID_CREDENTIALS:
418 logging.debug("Account credentials are invalid for %s" % self)
419 return False
420
421 logging.info("Successfully authenticated %s" % self)
422
423 return True
424
425 def check_password_quality(self, password):
426 """
427 Passwords are passed through zxcvbn to make sure
428 that they are strong enough.
429 """
430 return zxcvbn.zxcvbn(password, user_inputs=(
431 self.first_name, self.last_name,
432 ))
433
434 def is_admin(self):
435 return "wheel" in self.groups
436
437 def is_staff(self):
438 return "staff" in self.groups
439
440 def has_shell(self):
441 return "posixAccount" in self.classes
442
443 def has_mail(self):
444 return "postfixMailUser" in self.classes
445
446 def has_sip(self):
447 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
448
449 def can_be_managed_by(self, account):
450 """
451 Returns True if account is allowed to manage this account
452 """
453 # Admins can manage all accounts
454 if account.is_admin():
455 return True
456
457 # Users can manage themselves
458 return self == account
459
460 @property
461 def classes(self):
462 return self._get_strings("objectClass")
463
464 @property
465 def uid(self):
466 return self._get_string("uid")
467
468 @property
469 def name(self):
470 return self._get_string("cn")
471
472 # Nickname
473
474 def get_nickname(self):
475 return self._get_string("displayName")
476
477 def set_nickname(self, nickname):
478 self._set_string("displayName", nickname)
479
480 nickname = property(get_nickname, set_nickname)
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 photo = self._get_bytes("jpegPhoto")
788
789 # Exit if no avatar is available
790 if not photo:
791 return
792
793 # Return the raw image if no size was requested
794 if size is None:
795 return photo
796
797 # Try to retrieve something from the cache
798 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
799 if avatar:
800 return avatar
801
802 # Generate a new thumbnail
803 avatar = util.generate_thumbnail(photo, size, square=True)
804
805 # Save to cache for 15m
806 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
807
808 return avatar
809
810 def upload_avatar(self, avatar):
811 self._set("jpegPhoto", avatar)
812
813 # SSH Keys
814
815 @lazy_property
816 def ssh_keys(self):
817 ret = []
818
819 for key in self._get_strings("sshPublicKey"):
820 s = sshpubkeys.SSHKey()
821
822 try:
823 s.parse(key)
824 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
825 logging.warning("Could not parse SSH key %s: %s" % (key, e))
826 continue
827
828 ret.append(s)
829
830 return ret
831
832 def get_ssh_key_by_hash_sha256(self, hash_sha256):
833 for key in self.ssh_keys:
834 if not key.hash_sha256() == hash_sha256:
835 continue
836
837 return key
838
839 def add_ssh_key(self, key):
840 k = sshpubkeys.SSHKey()
841
842 # Try to parse the key
843 k.parse(key)
844
845 # Check for types and sufficient sizes
846 if k.key_type == b"ssh-rsa":
847 if k.bits < 4096:
848 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
849
850 elif k.key_type == b"ssh-dss":
851 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
852
853 # Ignore any duplicates
854 if key in (k.keydata for k in self.ssh_keys):
855 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
856 return
857
858 # Prepare transaction
859 modlist = []
860
861 # Add object class if user is not in it, yet
862 if not "ldapPublicKey" in self.classes:
863 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
864
865 # Add key
866 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
867
868 # Save key to LDAP
869 self._modify(modlist)
870
871 # Append to cache
872 self.ssh_keys.append(k)
873
874 def delete_ssh_key(self, key):
875 if not key in (k.keydata for k in self.ssh_keys):
876 return
877
878 # Delete key from LDAP
879 if len(self.ssh_keys) > 1:
880 self._delete_string("sshPublicKey", key)
881 else:
882 self._modify([
883 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
884 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
885 ])
886
887
888 if __name__ == "__main__":
889 a = Accounts()
890
891 print(a.list())