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