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