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