]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
people: Add account activation handler
[ipfire.org.git] / src / backend / accounts.py
CommitLineData
940227cb 1#!/usr/bin/python
78fdedae 2# encoding: utf-8
940227cb 3
2cd9af74 4import PIL
22153577 5import PIL.ImageOps
3ea97943 6import datetime
11347e46 7import io
940227cb 8import ldap
e96e445b 9import ldap.modlist
27066195 10import logging
e96e445b 11import phonenumbers
f4672785 12import sshpubkeys
eea71144
MT
13import urllib.parse
14import urllib.request
6b582a4f 15import zxcvbn
940227cb 16
e96e445b 17from . import util
917434b8 18from .decorators import *
11347e46 19from .misc import Object
940227cb 20
a6dc0bad 21class Accounts(Object):
9f05796c
MT
22 def __iter__(self):
23 # Only return developers (group with ID 1000)
1bae74c7 24 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
9f05796c 25
1bae74c7 26 return iter(sorted(accounts))
9f05796c 27
0ab42c1d 28 @lazy_property
66862195 29 def ldap(self):
0ab42c1d
MT
30 # Connect to LDAP server
31 ldap_uri = self.settings.get("ldap_uri")
32 conn = ldap.initialize(ldap_uri)
940227cb 33
0ab42c1d
MT
34 # Bind with username and password
35 bind_dn = self.settings.get("ldap_bind_dn")
36 if bind_dn:
37 bind_pw = self.settings.get("ldap_bind_pw", "")
38 conn.simple_bind(bind_dn, bind_pw)
66862195 39
0ab42c1d 40 return conn
940227cb 41
1bae74c7 42 def _query(self, query, attrlist=None, limit=0):
66862195 43 logging.debug("Performing LDAP query: %s" % query)
940227cb 44
66862195 45 search_base = self.settings.get("ldap_search_base")
a395634c 46
a69e87a1
MT
47 try:
48 results = self.ldap.search_ext_s(search_base, ldap.SCOPE_SUBTREE,
49 query, attrlist=attrlist, sizelimit=limit)
50 except:
51 # Close current connection
0ab42c1d 52 del self.ldap
a69e87a1
MT
53
54 raise
940227cb 55
66862195 56 return results
940227cb 57
1bae74c7 58 def _search(self, query, attrlist=None, limit=0):
66862195 59 accounts = []
1bae74c7
MT
60
61 for dn, attrs in self._query(query, attrlist=attrlist, limit=limit):
66862195
MT
62 account = Account(self.backend, dn, attrs)
63 accounts.append(account)
64
1bae74c7
MT
65 return accounts
66
67 def search(self, query):
68 # Search for exact matches
73a54cb6 69 accounts = self._search("(&(objectClass=person) \
1bae74c7
MT
70 (|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
71 % (query, query, query, query, query, query))
72
73 # Find accounts by name
74 if not accounts:
73a54cb6 75 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
1bae74c7
MT
76 if not account in accounts:
77 accounts.append(account)
78
66862195
MT
79 return sorted(accounts)
80
1bae74c7
MT
81 def _search_one(self, query):
82 result = self._search(query, limit=1)
66862195
MT
83 assert len(result) <= 1
84
85 if result:
86 return result[0]
87
f32dd17f
MT
88 def uid_exists(self, uid):
89 if self.get_by_uid(uid):
90 return True
91
92 res = self.db.get("SELECT 1 FROM account_activations \
93 WHERE uid = %s AND expires_at > NOW()", uid)
94
95 if res:
96 return True
97
98 # Account with uid does not exist, yet
99 return False
100
66862195 101 def get_by_uid(self, uid):
73a54cb6 102 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
66862195
MT
103
104 def get_by_mail(self, mail):
73a54cb6 105 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
66862195
MT
106
107 find = get_by_uid
108
109 def find_account(self, s):
110 account = self.get_by_uid(s)
111 if account:
112 return account
113
114 return self.get_by_mail(s)
940227cb 115
66862195 116 def get_by_sip_id(self, sip_id):
1bae74c7 117 return self._search_one("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
66862195 118 (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id, sip_id))
940227cb 119
525c01f7 120 def get_by_phone_number(self, number):
73a54cb6 121 return self._search_one("(&(objectClass=inetOrgPerson) \
525c01f7
MT
122 (|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
123 % (number, number, number, number))
124
f32dd17f
MT
125 # Registration
126
127 def create(self, uid, email, first_name, last_name):
128 # Check if UID is unique
129 if self.get_by_uid(uid):
130 raise ValueError("UID exists: %s" % uid)
131
132 activation_code = util.random_string(24)
133
134 # Account Parameters
135 account = {
136 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
137 "userPassword" : activation_code.encode(),
138 "mail" : email.encode(),
139
140 # Name
141 "cn" : b"%s %s" % (first_name.encode(), last_name.encode()),
142 "sn" : last_name.encode(),
143 "givenName" : first_name.encode(),
144 }
145
146 # Create account on LDAP
147 self.ldap.add_s("uid=%s,ou=People,dc=mcfly,dc=local" % uid, ldap.modlist.addModlist(account))
148
149 # TODO Send email with activation code
150 pass
151
66862195 152 # Session stuff
940227cb 153
66862195
MT
154 def _cleanup_expired_sessions(self):
155 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
940227cb 156
66862195
MT
157 def create_session(self, account, host):
158 self._cleanup_expired_sessions()
940227cb 159
66862195
MT
160 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
161 RETURNING session_id, time_expires", host, account.uid)
162
163 # Session could not be created
164 if not res:
165 return None, None
166
167 logging.info("Created session %s for %s which expires %s" \
168 % (res.session_id, account, res.time_expires))
169 return res.session_id, res.time_expires
170
171 def destroy_session(self, session_id, host):
172 logging.info("Destroying session %s" % session_id)
173
174 self.db.execute("DELETE FROM sessions \
175 WHERE session_id = %s AND host = %s", session_id, host)
176 self._cleanup_expired_sessions()
177
178 def get_by_session(self, session_id, host):
179 logging.debug("Looking up session %s" % session_id)
180
181 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
182 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
183 session_id, host)
184
185 # Session does not exist or has expired
186 if not res:
187 return
188
189 # Update the session expiration time
190 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
191 WHERE session_id = %s AND host = %s", session_id, host)
192
193 return self.get_by_uid(res.uid)
194
940227cb 195
a6dc0bad 196class Account(Object):
66862195 197 def __init__(self, backend, dn, attrs=None):
a6dc0bad 198 Object.__init__(self, backend)
940227cb
MT
199 self.dn = dn
200
e96e445b 201 self.attributes = attrs or {}
940227cb 202
917434b8
MT
203 def __str__(self):
204 return self.name
205
940227cb
MT
206 def __repr__(self):
207 return "<%s %s>" % (self.__class__.__name__, self.dn)
208
541c952b
MT
209 def __eq__(self, other):
210 if isinstance(other, self.__class__):
211 return self.dn == other.dn
212
213 def __lt__(self, other):
214 if isinstance(other, self.__class__):
215 return self.name < other.name
940227cb
MT
216
217 @property
66862195
MT
218 def ldap(self):
219 return self.accounts.ldap
940227cb 220
e96e445b
MT
221 def _exists(self, key):
222 try:
223 self.attributes[key]
224 except KeyError:
225 return False
940227cb 226
e96e445b 227 return True
940227cb 228
e96e445b
MT
229 def _get(self, key):
230 for value in self.attributes.get(key, []):
231 yield value
940227cb 232
e96e445b
MT
233 def _get_bytes(self, key, default=None):
234 for value in self._get(key):
235 return value
236
237 return default
238
239 def _get_strings(self, key):
240 for value in self._get(key):
241 yield value.decode()
242
243 def _get_string(self, key, default=None):
244 for value in self._get_strings(key):
245 return value
246
247 return default
248
249 def _get_phone_numbers(self, key):
250 for value in self._get_strings(key):
251 yield phonenumbers.parse(value, None)
252
253 def _modify(self, modlist):
254 logging.debug("Modifying %s: %s" % (self.dn, modlist))
255
256 # Run modify operation
257 self.ldap.modify_s(self.dn, modlist)
258
259 def _set(self, key, values):
260 current = self._get(key)
261
262 # Don't do anything if nothing has changed
263 if list(current) == values:
264 return
265
266 # Remove all old values and add all new ones
267 modlist = []
940227cb 268
e96e445b
MT
269 if self._exists(key):
270 modlist.append((ldap.MOD_DELETE, key, None))
940227cb 271
e96e445b 272 # Add new values
47bb098f
MT
273 if values:
274 modlist.append((ldap.MOD_ADD, key, values))
e96e445b
MT
275
276 # Run modify operation
277 self._modify(modlist)
278
279 # Update cache
280 self.attributes.update({ key : values })
281
282 def _set_bytes(self, key, values):
283 return self._set(key, values)
284
285 def _set_strings(self, key, values):
47bb098f 286 return self._set(key, [e.encode() for e in values if e])
e96e445b
MT
287
288 def _set_string(self, key, value):
289 return self._set_strings(key, [value,])
940227cb 290
0d1fb712
MT
291 def _add(self, key, values):
292 modlist = [
293 (ldap.MOD_ADD, key, values),
294 ]
295
296 self._modify(modlist)
297
298 def _add_strings(self, key, values):
299 return self._add(key, [e.encode() for e in values])
300
301 def _add_string(self, key, value):
302 return self._add_strings(key, [value,])
303
304 def _delete(self, key, values):
305 modlist = [
306 (ldap.MOD_DELETE, key, values),
307 ]
308
309 self._modify(modlist)
310
311 def _delete_strings(self, key, values):
312 return self._delete(key, [e.encode() for e in values])
313
314 def _delete_string(self, key, value):
315 return self._delete_strings(key, [value,])
316
6b582a4f 317 def passwd(self, password):
3ea97943
MT
318 """
319 Sets a new password
320 """
6b582a4f
MT
321 # The new password must have a score of 3 or better
322 quality = self.check_password_quality(password)
323 if quality["score"] < 3:
324 raise ValueError("Password too weak")
325
326 self.ldap.passwd_s(self.dn, None, password)
3ea97943 327
940227cb
MT
328 def check_password(self, password):
329 """
330 Bind to the server with given credentials and return
331 true if password is corrent and false if not.
332
333 Raises exceptions from the server on any other errors.
334 """
0d1fb712
MT
335 if not password:
336 return
337
940227cb 338 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
339
340 # Create a new LDAP connection
341 ldap_uri = self.backend.settings.get("ldap_uri")
342 conn = ldap.initialize(ldap_uri)
343
940227cb 344 try:
3ea97943 345 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 346 except ldap.INVALID_CREDENTIALS:
3ea97943 347 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
348 return False
349
3ea97943
MT
350 logging.info("Successfully authenticated %s" % self)
351
940227cb
MT
352 return True
353
6b582a4f
MT
354 def check_password_quality(self, password):
355 """
356 Passwords are passed through zxcvbn to make sure
357 that they are strong enough.
358 """
359 return zxcvbn.zxcvbn(password, user_inputs=(
360 self.first_name, self.last_name,
361 ))
362
940227cb 363 def is_admin(self):
d82bc8e3 364 return "wheel" in self.groups
66862195
MT
365
366 def is_talk_enabled(self):
06c1d39c
MT
367 return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
368 or self.telephone_numbers or self.address
66862195 369
e96e445b
MT
370 def can_be_managed_by(self, account):
371 """
372 Returns True if account is allowed to manage this account
373 """
374 # Admins can manage all accounts
375 if account.is_admin():
376 return True
377
378 # Users can manage themselves
379 return self == account
380
66862195
MT
381 @property
382 def classes(self):
e96e445b 383 return self._get_strings("objectClass")
66862195
MT
384
385 @property
386 def uid(self):
e96e445b 387 return self._get_string("uid")
940227cb 388
a6dc0bad
MT
389 @property
390 def name(self):
e96e445b 391 return self._get_string("cn")
66862195 392
e96e445b
MT
393 # First Name
394
395 def get_first_name(self):
396 return self._get_string("givenName")
397
398 def set_first_name(self, first_name):
399 self._set_string("givenName", first_name)
400
401 # Update Common Name
402 self._set_string("cn", "%s %s" % (first_name, self.last_name))
403
404 first_name = property(get_first_name, set_first_name)
405
406 # Last Name
407
408 def get_last_name(self):
409 return self._get_string("sn")
410
411 def set_last_name(self, last_name):
412 self._set_string("sn", last_name)
413
414 # Update Common Name
415 self._set_string("cn", "%s %s" % (self.first_name, last_name))
416
417 last_name = property(get_last_name, set_last_name)
66862195 418
1bae74c7 419 @lazy_property
66862195 420 def groups(self):
1bae74c7 421 groups = []
66862195 422
1bae74c7
MT
423 res = self.accounts._query("(&(objectClass=posixGroup) \
424 (memberUid=%s))" % self.uid, ["cn"])
66862195 425
1bae74c7
MT
426 for dn, attrs in res:
427 cns = attrs.get("cn")
428 if cns:
429 groups.append(cns[0].decode())
66862195 430
1bae74c7 431 return groups
66862195 432
e96e445b
MT
433 # Address
434
435 def get_address(self):
436 address = self._get_string("homePostalAddress")
437
438 if address:
439 return (line.strip() for line in address.split(","))
66862195 440
e96e445b
MT
441 return []
442
443 def set_address(self, address):
444 data = ", ".join(address.splitlines())
445
446 self._set_bytes("homePostalAddress", data.encode())
447
448 address = property(get_address, set_address)
a6dc0bad 449
940227cb
MT
450 @property
451 def email(self):
66862195 452 name = self.name.lower()
940227cb 453 name = name.replace(" ", ".")
78fdedae
MT
454 name = name.replace("Ä", "Ae")
455 name = name.replace("Ö", "Oe")
456 name = name.replace("Ü", "Ue")
457 name = name.replace("ä", "ae")
458 name = name.replace("ö", "oe")
459 name = name.replace("ü", "ue")
940227cb 460
66862195 461 for mail in self.attributes.get("mail", []):
7aee4b8d 462 if mail.decode().startswith("%s@ipfire.org" % name):
940227cb
MT
463 return mail
464
2cd9af74
MT
465 # If everything else fails, we will go with the UID
466 return "%s@ipfire.org" % self.uid
940227cb 467
e96e445b
MT
468 # Mail Routing Address
469
470 def get_mail_routing_address(self):
471 return self._get_string("mailRoutingAddress", None)
472
473 def set_mail_routing_address(self, address):
47bb098f 474 self._set_string("mailRoutingAddress", address or None)
e96e445b
MT
475
476 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
477
66862195
MT
478 @property
479 def sip_id(self):
480 if "sipUser" in self.classes:
e96e445b 481 return self._get_string("sipAuthenticationUser")
66862195
MT
482
483 if "sipRoutingObject" in self.classes:
e96e445b 484 return self._get_string("sipLocalAddress")
66862195 485
2f51147a
MT
486 @property
487 def sip_password(self):
e96e445b
MT
488 return self._get_string("sipPassword")
489
490 @staticmethod
491 def _generate_sip_password():
492 return util.random_string(8)
2f51147a 493
66862195
MT
494 @property
495 def sip_url(self):
496 return "%s@ipfire.org" % self.sip_id
497
498 def uses_sip_forwarding(self):
e96e445b 499 if self.sip_routing_address:
66862195
MT
500 return True
501
502 return False
503
e96e445b
MT
504 # SIP Routing
505
506 def get_sip_routing_address(self):
66862195 507 if "sipRoutingObject" in self.classes:
e96e445b
MT
508 return self._get_string("sipRoutingAddress")
509
510 def set_sip_routing_address(self, address):
511 if not address:
512 address = None
513
514 # Don't do anything if nothing has changed
515 if self.get_sip_routing_address() == address:
516 return
517
518 if address:
519 modlist = [
520 # This is no longer a SIP user any more
521 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
522 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
523 (ldap.MOD_DELETE, "sipPassword", None),
524
525 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
526 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
527 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
528 ]
529 else:
530 modlist = [
531 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
532 (ldap.MOD_DELETE, "sipLocalAddress", None),
533 (ldap.MOD_DELETE, "sipRoutingAddress", None),
534
535 (ldap.MOD_ADD, "objectClass", b"sipUser"),
536 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
537 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
538 ]
539
540 # Run modification
541 self._modify(modlist)
542
543 # XXX Cache is invalid here
544
545 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 546
917434b8
MT
547 @lazy_property
548 def sip_registrations(self):
549 sip_registrations = []
550
551 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
552 reg.account = self
553
554 sip_registrations.append(reg)
555
556 return sip_registrations
557
1f38be5a
MT
558 @lazy_property
559 def sip_channels(self):
560 return self.backend.talk.freeswitch.get_sip_channels(self)
561
bdaf6b46
MT
562 def get_cdr(self, date=None, limit=None):
563 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 564
e96e445b 565 # Phone Numbers
6ff61434 566
d3208ac7
MT
567 @lazy_property
568 def phone_number(self):
569 """
570 Returns the IPFire phone number
571 """
572 if self.sip_id:
573 return phonenumbers.parse("+4923636035%s" % self.sip_id)
574
575 @lazy_property
576 def fax_number(self):
577 if self.sip_id:
578 return phonenumbers.parse("+49236360359%s" % self.sip_id)
579
e96e445b
MT
580 def get_phone_numbers(self):
581 ret = []
6ff61434 582
e96e445b
MT
583 for field in ("telephoneNumber", "homePhone", "mobile"):
584 for number in self._get_phone_numbers(field):
585 ret.append(number)
6ff61434 586
e96e445b
MT
587 return ret
588
589 def set_phone_numbers(self, phone_numbers):
590 # Sort phone numbers by landline and mobile
591 _landline_numbers = []
592 _mobile_numbers = []
593
594 for number in phone_numbers:
595 try:
596 number = phonenumbers.parse(number, None)
597 except phonenumbers.phonenumberutil.NumberParseException:
598 continue
599
600 # Convert to string (in E.164 format)
601 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
602
603 # Separate mobile numbers
604 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
605 _mobile_numbers.append(s)
606 else:
607 _landline_numbers.append(s)
608
609 # Save
610 self._set_strings("telephoneNumber", _landline_numbers)
611 self._set_strings("mobile", _mobile_numbers)
612
613 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
614
615 @property
616 def _all_telephone_numbers(self):
6ccc8acb
MT
617 ret = [ self.sip_id, ]
618
d3208ac7
MT
619 if self.phone_number:
620 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
621 ret.append(s)
622
6ccc8acb
MT
623 for number in self.phone_numbers:
624 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
625 ret.append(s)
626
627 return ret
66862195 628
2cd9af74
MT
629 def avatar_url(self, size=None):
630 if self.backend.debug:
03706893 631 hostname = "http://people.dev.ipfire.org"
2cd9af74 632 else:
03706893 633 hostname = "https://people.ipfire.org"
2cd9af74 634
03706893 635 url = "%s/users/%s.jpg" % (hostname, self.uid)
2cd9af74
MT
636
637 if size:
638 url += "?size=%s" % size
639
640 return url
641
2cd9af74 642 def get_avatar(self, size=None):
e96e445b 643 avatar = self._get_bytes("jpegPhoto")
2cd9af74
MT
644 if not avatar:
645 return
646
647 if not size:
648 return avatar
649
650 return self._resize_avatar(avatar, size)
651
652 def _resize_avatar(self, image, size):
1a226c83
MT
653 image = PIL.Image.open(io.BytesIO(image))
654
655 # Convert RGBA images into RGB because JPEG doesn't support alpha-channels
656 if image.mode == "RGBA":
657 image = image.convert("RGB")
658
22153577
MT
659 # Resize the image to the desired resolution (and make it square)
660 thumbnail = PIL.ImageOps.fit(image, (size, size), PIL.Image.ANTIALIAS)
1a226c83
MT
661
662 with io.BytesIO() as f:
663 # If writing out the image does not work with optimization,
664 # we try to write it out without any optimization.
665 try:
22153577 666 thumbnail.save(f, "JPEG", optimize=True, quality=98)
1a226c83 667 except:
22153577 668 thumbnail.save(f, "JPEG", quality=98)
1a226c83
MT
669
670 return f.getvalue()
2cd9af74 671
5cc10421
MT
672 def upload_avatar(self, avatar):
673 self._set("jpegPhoto", avatar)
674
f4672785
MT
675 # SSH Keys
676
677 @lazy_property
678 def ssh_keys(self):
679 ret = []
680
681 for key in self._get_strings("sshPublicKey"):
682 s = sshpubkeys.SSHKey()
683
684 try:
685 s.parse(key)
686 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
687 logging.warning("Could not parse SSH key %s: %s" % (key, e))
688 continue
689
690 ret.append(s)
691
692 return ret
60024cc8 693
44b75370 694 def get_ssh_key_by_hash_sha256(self, hash_sha256):
55b67ca4 695 for key in self.ssh_keys:
44b75370 696 if not key.hash_sha256() == hash_sha256:
55b67ca4
MT
697 continue
698
699 return key
700
0d1fb712
MT
701 def add_ssh_key(self, key):
702 k = sshpubkeys.SSHKey()
703
704 # Try to parse the key
705 k.parse(key)
706
707 # Check for types and sufficient sizes
708 if k.key_type == b"ssh-rsa":
709 if k.bits < 4096:
710 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
711
712 elif k.key_type == b"ssh-dss":
713 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
714
715 # Ignore any duplicates
716 if key in (k.keydata for k in self.ssh_keys):
717 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
718 return
719
cc27cb63
MT
720 # Prepare transaction
721 modlist = []
722
723 # Add object class if user is not in it, yet
724 if not "ldapPublicKey" in self.classes:
725 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
726
727 # Add key
728 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
729
0d1fb712 730 # Save key to LDAP
cc27cb63 731 self._modify(modlist)
0d1fb712
MT
732
733 # Append to cache
734 self.ssh_keys.append(k)
735
736 def delete_ssh_key(self, key):
737 if not key in (k.keydata for k in self.ssh_keys):
738 return
739
740 # Delete key from LDAP
cc27cb63
MT
741 if len(self.ssh_keys) > 1:
742 self._delete_string("sshPublicKey", key)
743 else:
744 self._modify([
745 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
746 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
747 ])
0d1fb712 748
55b67ca4 749
940227cb
MT
750if __name__ == "__main__":
751 a = Accounts()
752
11347e46 753 print(a.list())