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