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