]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
download: Do not check any RBLs any more
[ipfire.org.git] / src / backend / accounts.py
CommitLineData
940227cb 1#!/usr/bin/python
78fdedae 2# encoding: utf-8
940227cb 3
2dac7110 4import base64
3ea97943 5import datetime
2dac7110 6import hmac
23f84bbc 7import json
940227cb 8import ldap
e96e445b 9import ldap.modlist
27066195 10import logging
30e11b1b 11import os
e96e445b 12import phonenumbers
f4672785 13import sshpubkeys
f0c9d237 14import time
23f84bbc 15import tornado.httpclient
eea71144
MT
16import urllib.parse
17import urllib.request
6b582a4f 18import zxcvbn
940227cb 19
0099c2a7 20from . import countries
e96e445b 21from . import util
917434b8 22from .decorators import *
11347e46 23from .misc import Object
940227cb 24
30e11b1b
MT
25# Set the client keytab name
26os.environ["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
27
a6dc0bad 28class Accounts(Object):
58d22b5d
MT
29 def init(self):
30 self.search_base = self.settings.get("ldap_search_base")
31
9f05796c
MT
32 def __iter__(self):
33 # Only return developers (group with ID 1000)
1bae74c7 34 accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
9f05796c 35
1bae74c7 36 return iter(sorted(accounts))
9f05796c 37
0ab42c1d 38 @lazy_property
66862195 39 def ldap(self):
0ab42c1d
MT
40 # Connect to LDAP server
41 ldap_uri = self.settings.get("ldap_uri")
940227cb 42
867c06a1
MT
43 logging.debug("Connecting to LDAP server: %s" % ldap_uri)
44
6c9a8663 45 # Connect to the LDAP server
6e33e8e1 46 return ldap.ldapobject.ReconnectLDAPObject(ldap_uri,
6c9a8663
MT
47 retry_max=10, retry_delay=3)
48
6e33e8e1 49 def _authenticate(self):
30e11b1b
MT
50 # Authenticate against LDAP server using Kerberos
51 self.ldap.sasl_gssapi_bind_s()
52
53 def test_ldap(self):
54 logging.info("Testing LDAP connection...")
55
56 self._authenticate()
57
58 logging.info("Successfully authenticated as %s" % self.ldap.whoami_s())
940227cb 59
a3bbc04e 60 def _query(self, query, attrlist=None, limit=0, search_base=None):
9150881e
MT
61 logging.debug("Performing LDAP query (%s): %s" \
62 % (search_base or self.search_base, query))
940227cb 63
f0c9d237 64 t = time.time()
a69e87a1 65
91f72160 66 results = self.ldap.search_ext_s(search_base or self.search_base,
a3bbc04e 67 ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit)
f0c9d237
MT
68
69 # Log time it took to perform the query
70 logging.debug("Query took %.2fms" % ((time.time() - t) * 1000.0))
940227cb 71
66862195 72 return results
940227cb 73
1bae74c7 74 def _search(self, query, attrlist=None, limit=0):
a3bbc04e
MT
75 accounts = []
76 for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
77 account = self.get_by_dn(dn)
78 accounts.append(account)
91f72160 79
a3bbc04e 80 return accounts
0dcf4344 81
a3bbc04e
MT
82 def _get_attrs(self, dn):
83 """
84 Fetches all attributes for the given distinguished name
85 """
9150881e
MT
86 results = self._query("(objectClass=*)", search_base=dn, limit=1,
87 attrlist=("*", "createTimestamp", "modifyTimestamp"))
91f72160 88
a3bbc04e
MT
89 for dn, attrs in results:
90 return attrs
91f72160 91
2cdf68d8 92 def get_by_dn(self, dn):
91f72160
MT
93 attrs = self.memcache.get("accounts:%s:attrs" % dn)
94 if attrs is None:
95 attrs = self._get_attrs(dn)
96 assert attrs, dn
97
98 # Cache all attributes for 5 min
99 self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
100
101 return Account(self.backend, dn, attrs)
102
9150881e
MT
103 def get_created_after(self, ts):
104 t = ts.strftime("%Y%m%d%H%M%SZ")
105
106 return self._search("(&(objectClass=person)(createTimestamp>=%s))" % t)
107
1bae74c7
MT
108 def search(self, query):
109 # Search for exact matches
df70e85e
MT
110 accounts = self._search(
111 "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
1bae74c7
MT
112 % (query, query, query, query, query, query))
113
114 # Find accounts by name
115 if not accounts:
73a54cb6 116 for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
1bae74c7
MT
117 if not account in accounts:
118 accounts.append(account)
119
66862195
MT
120 return sorted(accounts)
121
1bae74c7 122 def _search_one(self, query):
18209c78 123 results = self._search(query, limit=1)
66862195 124
18209c78
MT
125 for result in results:
126 return result
66862195 127
f32dd17f
MT
128 def uid_exists(self, uid):
129 if self.get_by_uid(uid):
130 return True
131
132 res = self.db.get("SELECT 1 FROM account_activations \
133 WHERE uid = %s AND expires_at > NOW()", uid)
134
135 if res:
136 return True
137
138 # Account with uid does not exist, yet
139 return False
140
66862195 141 def get_by_uid(self, uid):
73a54cb6 142 return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
66862195
MT
143
144 def get_by_mail(self, mail):
73a54cb6 145 return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
66862195 146
66862195
MT
147 def find_account(self, s):
148 account = self.get_by_uid(s)
149 if account:
150 return account
151
152 return self.get_by_mail(s)
940227cb 153
66862195 154 def get_by_sip_id(self, sip_id):
df70e85e
MT
155 if not sip_id:
156 return
157
158 return self._search_one(
159 "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
160 % (sip_id, sip_id))
940227cb 161
525c01f7 162 def get_by_phone_number(self, number):
df70e85e
MT
163 if not number:
164 return
165
166 return self._search_one(
167 "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
525c01f7
MT
168 % (number, number, number, number))
169
23f84bbc
MT
170 @tornado.gen.coroutine
171 def check_spam(self, uid, email, address):
172 sfs = StopForumSpam(self.backend, uid, email, address)
173
174 # Get spam score
175 score = yield sfs.check()
176
177 return score >= 50
178
328a7710
MT
179 def auth(self, username, password):
180 # Find account
181 account = self.backend.accounts.find_account(username)
182
183 # Check credentials
184 if account and account.check_password(password):
185 return account
186
f32dd17f
MT
187 # Registration
188
757372cd 189 def register(self, uid, email, first_name, last_name, country_code=None):
16048b22
MT
190 # Convert all uids to lowercase
191 uid = uid.lower()
192
f32dd17f 193 # Check if UID is unique
16048b22 194 if self.uid_exists(uid):
f32dd17f
MT
195 raise ValueError("UID exists: %s" % uid)
196
718d1375
MT
197 # Generate a random activation code
198 activation_code = util.random_string(36)
199
200 # Create an entry in our database until the user
201 # has activated the account
202 self.db.execute("INSERT INTO account_activations(uid, activation_code, \
757372cd
MT
203 email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
204 uid, activation_code, email, first_name, last_name, country_code)
718d1375
MT
205
206 # Send an account activation email
207 self.backend.messages.send_template("auth/messages/register",
208 recipients=[email], priority=100, uid=uid,
209 activation_code=activation_code, email=email,
210 first_name=first_name, last_name=last_name)
211
b4d72c76
MT
212 def activate(self, uid, activation_code):
213 res = self.db.get("DELETE FROM account_activations \
214 WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
215 RETURNING *", uid, activation_code)
216
217 # Return nothing when account was not found
218 if not res:
219 return
f32dd17f 220
b4d72c76 221 # Create a new account on the LDAP database
fd86345d 222 account = self.create(uid, res.email,
757372cd
MT
223 first_name=res.first_name, last_name=res.last_name,
224 country_code=res.country_code)
b4d72c76 225
fd86345d
MT
226 # Send email about account registration
227 self.backend.messages.send_template("people/messages/new-account",
228 recipients=["admin@ipfire.org"], account=account)
229
230 return account
231
757372cd 232 def create(self, uid, email, first_name, last_name, country_code=None):
a151df3f
MT
233 cn = "%s %s" % (first_name, last_name)
234
f32dd17f
MT
235 # Account Parameters
236 account = {
237 "objectClass" : [b"top", b"person", b"inetOrgPerson"],
f32dd17f
MT
238 "mail" : email.encode(),
239
240 # Name
a151df3f 241 "cn" : cn.encode(),
f32dd17f
MT
242 "sn" : last_name.encode(),
243 "givenName" : first_name.encode(),
244 }
245
b4d72c76
MT
246 logging.info("Creating new account: %s: %s" % (uid, account))
247
248 # Create DN
c51fd4bf 249 dn = "uid=%s,ou=People,dc=ipfire,dc=org" % uid
b4d72c76 250
f32dd17f 251 # Create account on LDAP
6e33e8e1 252 self.accounts._authenticate()
b4d72c76 253 self.ldap.add_s(dn, ldap.modlist.addModlist(account))
f32dd17f 254
757372cd
MT
255 # Fetch the account
256 account = self.get_by_dn(dn)
257
258 # Optionally set country code
259 if country_code:
260 account.country_code = country_code
261
b4d72c76 262 # Return account
757372cd 263 return account
f32dd17f 264
66862195 265 # Session stuff
940227cb 266
66862195 267 def create_session(self, account, host):
66862195
MT
268 res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
269 RETURNING session_id, time_expires", host, account.uid)
270
271 # Session could not be created
272 if not res:
273 return None, None
274
275 logging.info("Created session %s for %s which expires %s" \
276 % (res.session_id, account, res.time_expires))
277 return res.session_id, res.time_expires
278
279 def destroy_session(self, session_id, host):
280 logging.info("Destroying session %s" % session_id)
281
282 self.db.execute("DELETE FROM sessions \
283 WHERE session_id = %s AND host = %s", session_id, host)
66862195
MT
284
285 def get_by_session(self, session_id, host):
286 logging.debug("Looking up session %s" % session_id)
287
288 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
289 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
290 session_id, host)
291
292 # Session does not exist or has expired
293 if not res:
294 return
295
296 # Update the session expiration time
297 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
298 WHERE session_id = %s AND host = %s", session_id, host)
299
300 return self.get_by_uid(res.uid)
d86f6f18 301
8e69850a
MT
302 def cleanup(self):
303 # Cleanup expired sessions
304 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
305
306 # Cleanup expired account activations
307 self.db.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
308
2dac7110
MT
309 # Discourse
310
311 def decode_discourse_payload(self, payload, signature):
312 # Check signature
313 calculated_signature = self.sign_discourse_payload(payload)
314
315 if not hmac.compare_digest(signature, calculated_signature):
316 raise ValueError("Invalid signature: %s" % signature)
317
318 # Decode the query string
319 qs = base64.b64decode(payload).decode()
320
321 # Parse the query string
322 data = {}
323 for key, val in urllib.parse.parse_qsl(qs):
324 data[key] = val
325
326 return data
327
328 def encode_discourse_payload(self, **args):
329 # Encode the arguments into an URL-formatted string
330 qs = urllib.parse.urlencode(args).encode()
331
332 # Encode into base64
333 return base64.b64encode(qs).decode()
334
335 def sign_discourse_payload(self, payload, secret=None):
336 if secret is None:
337 secret = self.settings.get("discourse_sso_secret")
338
339 # Calculate a HMAC using SHA256
340 h = hmac.new(secret.encode(),
341 msg=payload.encode(), digestmod="sha256")
342
343 return h.hexdigest()
344
940227cb 345
a6dc0bad 346class Account(Object):
66862195 347 def __init__(self, backend, dn, attrs=None):
a6dc0bad 348 Object.__init__(self, backend)
940227cb
MT
349 self.dn = dn
350
e96e445b 351 self.attributes = attrs or {}
940227cb 352
917434b8 353 def __str__(self):
d6e57f73
MT
354 if self.nickname:
355 return self.nickname
356
917434b8
MT
357 return self.name
358
940227cb
MT
359 def __repr__(self):
360 return "<%s %s>" % (self.__class__.__name__, self.dn)
361
541c952b
MT
362 def __eq__(self, other):
363 if isinstance(other, self.__class__):
364 return self.dn == other.dn
365
366 def __lt__(self, other):
367 if isinstance(other, self.__class__):
368 return self.name < other.name
940227cb
MT
369
370 @property
66862195
MT
371 def ldap(self):
372 return self.accounts.ldap
940227cb 373
e96e445b
MT
374 def _exists(self, key):
375 try:
376 self.attributes[key]
377 except KeyError:
378 return False
940227cb 379
e96e445b 380 return True
940227cb 381
e96e445b
MT
382 def _get(self, key):
383 for value in self.attributes.get(key, []):
384 yield value
940227cb 385
e96e445b
MT
386 def _get_bytes(self, key, default=None):
387 for value in self._get(key):
388 return value
389
390 return default
391
392 def _get_strings(self, key):
393 for value in self._get(key):
394 yield value.decode()
395
396 def _get_string(self, key, default=None):
397 for value in self._get_strings(key):
398 return value
399
400 return default
401
402 def _get_phone_numbers(self, key):
403 for value in self._get_strings(key):
404 yield phonenumbers.parse(value, None)
405
9150881e
MT
406 def _get_timestamp(self, key):
407 value = self._get_string(key)
408
409 # Parse the timestamp value and returns a datetime object
f345d70a
MT
410 if value:
411 return datetime.datetime.strptime(value, "%Y%m%d%H%M%SZ")
9150881e 412
e96e445b
MT
413 def _modify(self, modlist):
414 logging.debug("Modifying %s: %s" % (self.dn, modlist))
415
6e33e8e1
MT
416 # Authenticate before performing any write operations
417 self.accounts._authenticate()
418
e96e445b
MT
419 # Run modify operation
420 self.ldap.modify_s(self.dn, modlist)
421
91f72160 422 # Delete cached attributes
9c01e5ac 423 self.memcache.delete("accounts:%s:attrs" % self.dn)
91f72160 424
e96e445b
MT
425 def _set(self, key, values):
426 current = self._get(key)
427
428 # Don't do anything if nothing has changed
429 if list(current) == values:
430 return
431
432 # Remove all old values and add all new ones
433 modlist = []
940227cb 434
e96e445b
MT
435 if self._exists(key):
436 modlist.append((ldap.MOD_DELETE, key, None))
940227cb 437
e96e445b 438 # Add new values
47bb098f
MT
439 if values:
440 modlist.append((ldap.MOD_ADD, key, values))
e96e445b
MT
441
442 # Run modify operation
443 self._modify(modlist)
444
445 # Update cache
446 self.attributes.update({ key : values })
447
448 def _set_bytes(self, key, values):
449 return self._set(key, values)
450
451 def _set_strings(self, key, values):
47bb098f 452 return self._set(key, [e.encode() for e in values if e])
e96e445b
MT
453
454 def _set_string(self, key, value):
455 return self._set_strings(key, [value,])
940227cb 456
0d1fb712
MT
457 def _add(self, key, values):
458 modlist = [
459 (ldap.MOD_ADD, key, values),
460 ]
461
462 self._modify(modlist)
463
464 def _add_strings(self, key, values):
465 return self._add(key, [e.encode() for e in values])
466
467 def _add_string(self, key, value):
468 return self._add_strings(key, [value,])
469
470 def _delete(self, key, values):
471 modlist = [
472 (ldap.MOD_DELETE, key, values),
473 ]
474
475 self._modify(modlist)
476
477 def _delete_strings(self, key, values):
478 return self._delete(key, [e.encode() for e in values])
479
480 def _delete_string(self, key, value):
481 return self._delete_strings(key, [value,])
482
6b582a4f 483 def passwd(self, password):
3ea97943
MT
484 """
485 Sets a new password
486 """
6b582a4f
MT
487 # The new password must have a score of 3 or better
488 quality = self.check_password_quality(password)
489 if quality["score"] < 3:
490 raise ValueError("Password too weak")
491
1babcd04 492 self.accounts._authenticate()
6b582a4f 493 self.ldap.passwd_s(self.dn, None, password)
3ea97943 494
940227cb
MT
495 def check_password(self, password):
496 """
497 Bind to the server with given credentials and return
498 true if password is corrent and false if not.
499
500 Raises exceptions from the server on any other errors.
501 """
0d1fb712
MT
502 if not password:
503 return
504
940227cb 505 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
506
507 # Create a new LDAP connection
508 ldap_uri = self.backend.settings.get("ldap_uri")
509 conn = ldap.initialize(ldap_uri)
510
940227cb 511 try:
3ea97943 512 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 513 except ldap.INVALID_CREDENTIALS:
3ea97943 514 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
515 return False
516
3ea97943
MT
517 logging.info("Successfully authenticated %s" % self)
518
940227cb
MT
519 return True
520
6b582a4f
MT
521 def check_password_quality(self, password):
522 """
523 Passwords are passed through zxcvbn to make sure
524 that they are strong enough.
525 """
526 return zxcvbn.zxcvbn(password, user_inputs=(
527 self.first_name, self.last_name,
528 ))
529
940227cb 530 def is_admin(self):
2dac7110 531 return "sudo" in self.groups
66862195 532
71a3109c
MT
533 def is_staff(self):
534 return "staff" in self.groups
535
536 def has_shell(self):
537 return "posixAccount" in self.classes
538
539 def has_mail(self):
540 return "postfixMailUser" in self.classes
541
542 def has_sip(self):
543 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
66862195 544
e96e445b
MT
545 def can_be_managed_by(self, account):
546 """
547 Returns True if account is allowed to manage this account
548 """
549 # Admins can manage all accounts
550 if account.is_admin():
551 return True
552
553 # Users can manage themselves
554 return self == account
555
66862195
MT
556 @property
557 def classes(self):
e96e445b 558 return self._get_strings("objectClass")
66862195
MT
559
560 @property
561 def uid(self):
e96e445b 562 return self._get_string("uid")
940227cb 563
a6dc0bad
MT
564 @property
565 def name(self):
e96e445b 566 return self._get_string("cn")
66862195 567
d6e57f73
MT
568 # Nickname
569
570 def get_nickname(self):
571 return self._get_string("displayName")
572
573 def set_nickname(self, nickname):
574 self._set_string("displayName", nickname)
575
576 nickname = property(get_nickname, set_nickname)
577
e96e445b
MT
578 # First Name
579
580 def get_first_name(self):
581 return self._get_string("givenName")
582
583 def set_first_name(self, first_name):
584 self._set_string("givenName", first_name)
585
586 # Update Common Name
587 self._set_string("cn", "%s %s" % (first_name, self.last_name))
588
589 first_name = property(get_first_name, set_first_name)
590
591 # Last Name
592
593 def get_last_name(self):
594 return self._get_string("sn")
595
596 def set_last_name(self, last_name):
597 self._set_string("sn", last_name)
598
599 # Update Common Name
600 self._set_string("cn", "%s %s" % (self.first_name, last_name))
601
602 last_name = property(get_last_name, set_last_name)
66862195 603
1bae74c7 604 @lazy_property
66862195 605 def groups(self):
819daf36
MT
606 groups = self.memcache.get("accounts:%s:groups" % self.dn)
607 if groups:
608 return groups
609
610 # Fetch groups from LDAP
611 groups = self._get_groups()
612
613 # Cache groups for 5 min
614 self.memcache.set("accounts:%s:groups" % self.dn, groups, 300)
615
616 return groups
617
618 def _get_groups(self):
1bae74c7 619 groups = []
66862195 620
1bae74c7
MT
621 res = self.accounts._query("(&(objectClass=posixGroup) \
622 (memberUid=%s))" % self.uid, ["cn"])
66862195 623
1bae74c7
MT
624 for dn, attrs in res:
625 cns = attrs.get("cn")
626 if cns:
627 groups.append(cns[0].decode())
66862195 628
1bae74c7 629 return groups
66862195 630
9150881e
MT
631 # Created/Modified at
632
633 @property
634 def created_at(self):
635 return self._get_timestamp("createTimestamp")
636
637 @property
638 def modified_at(self):
639 return self._get_timestamp("modifyTimestamp")
640
e96e445b
MT
641 # Address
642
0099c2a7
MT
643 @property
644 def address(self):
645 address = []
646
647 if self.street:
648 address += self.street.splitlines()
649
650 if self.postal_code and self.city:
651 if self.country_code in ("AT", "DE"):
652 address.append("%s %s" % (self.postal_code, self.city))
653 else:
654 address.append("%s, %s" % (self.city, self.postal_code))
655 else:
656 address.append(self.city or self.postal_code)
657
658 if self.country_name:
659 address.append(self.country_name)
660
661 return address
662
663 def get_street(self):
664 return self._get_string("street") or self._get_string("homePostalAddress")
665
666 def set_street(self, street):
667 self._set_string("street", street)
e96e445b 668
0099c2a7 669 street = property(get_street, set_street)
66862195 670
0099c2a7
MT
671 def get_city(self):
672 return self._get_string("l") or ""
e96e445b 673
0099c2a7
MT
674 def set_city(self, city):
675 self._set_string("l", city)
e96e445b 676
0099c2a7 677 city = property(get_city, set_city)
e96e445b 678
0099c2a7
MT
679 def get_postal_code(self):
680 return self._get_string("postalCode") or ""
681
682 def set_postal_code(self, postal_code):
683 self._set_string("postalCode", postal_code)
684
685 postal_code = property(get_postal_code, set_postal_code)
686
687 # XXX This should be c
688 def get_country_code(self):
689 return self._get_string("st")
690
691 def set_country_code(self, country_code):
692 self._set_string("st", country_code)
693
694 country_code = property(get_country_code, set_country_code)
695
696 @property
697 def country_name(self):
698 if self.country_code:
699 return countries.get_name(self.country_code)
a6dc0bad 700
940227cb
MT
701 @property
702 def email(self):
d86f6f18 703 return self._get_string("mail")
940227cb 704
e96e445b
MT
705 # Mail Routing Address
706
707 def get_mail_routing_address(self):
708 return self._get_string("mailRoutingAddress", None)
709
710 def set_mail_routing_address(self, address):
47bb098f 711 self._set_string("mailRoutingAddress", address or None)
e96e445b
MT
712
713 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
714
66862195
MT
715 @property
716 def sip_id(self):
717 if "sipUser" in self.classes:
e96e445b 718 return self._get_string("sipAuthenticationUser")
66862195
MT
719
720 if "sipRoutingObject" in self.classes:
e96e445b 721 return self._get_string("sipLocalAddress")
66862195 722
2f51147a
MT
723 @property
724 def sip_password(self):
e96e445b
MT
725 return self._get_string("sipPassword")
726
727 @staticmethod
728 def _generate_sip_password():
729 return util.random_string(8)
2f51147a 730
66862195
MT
731 @property
732 def sip_url(self):
733 return "%s@ipfire.org" % self.sip_id
734
735 def uses_sip_forwarding(self):
e96e445b 736 if self.sip_routing_address:
66862195
MT
737 return True
738
739 return False
740
e96e445b
MT
741 # SIP Routing
742
743 def get_sip_routing_address(self):
66862195 744 if "sipRoutingObject" in self.classes:
e96e445b
MT
745 return self._get_string("sipRoutingAddress")
746
747 def set_sip_routing_address(self, address):
748 if not address:
749 address = None
750
751 # Don't do anything if nothing has changed
752 if self.get_sip_routing_address() == address:
753 return
754
755 if address:
79cce555
MT
756 # This is no longer a SIP user any more
757 try:
758 self._modify([
759 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
760 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
761 (ldap.MOD_DELETE, "sipPassword", None),
762 ])
763 except ldap.NO_SUCH_ATTRIBUTE:
764 pass
765
766 # Set new routing object
767 try:
768 self._modify([
769 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
770 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
771 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
772 ])
773
774 # If this is a change, we cannot add this again
775 except ldap.TYPE_OR_VALUE_EXISTS:
776 self._set_string("sipRoutingAddress", address)
e96e445b 777 else:
79cce555
MT
778 try:
779 self._modify([
780 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
781 (ldap.MOD_DELETE, "sipLocalAddress", None),
782 (ldap.MOD_DELETE, "sipRoutingAddress", None),
783 ])
784 except ldap.NO_SUCH_ATTRIBUTE:
785 pass
786
787 self._modify([
e96e445b
MT
788 (ldap.MOD_ADD, "objectClass", b"sipUser"),
789 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
790 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
79cce555 791 ])
e96e445b
MT
792
793 # XXX Cache is invalid here
794
795 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 796
917434b8
MT
797 @lazy_property
798 def sip_registrations(self):
799 sip_registrations = []
800
801 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
802 reg.account = self
803
804 sip_registrations.append(reg)
805
806 return sip_registrations
807
1f38be5a
MT
808 @lazy_property
809 def sip_channels(self):
810 return self.backend.talk.freeswitch.get_sip_channels(self)
811
bdaf6b46
MT
812 def get_cdr(self, date=None, limit=None):
813 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 814
e96e445b 815 # Phone Numbers
6ff61434 816
d3208ac7
MT
817 @lazy_property
818 def phone_number(self):
819 """
820 Returns the IPFire phone number
821 """
822 if self.sip_id:
823 return phonenumbers.parse("+4923636035%s" % self.sip_id)
824
825 @lazy_property
826 def fax_number(self):
827 if self.sip_id:
828 return phonenumbers.parse("+49236360359%s" % self.sip_id)
829
e96e445b
MT
830 def get_phone_numbers(self):
831 ret = []
6ff61434 832
e96e445b
MT
833 for field in ("telephoneNumber", "homePhone", "mobile"):
834 for number in self._get_phone_numbers(field):
835 ret.append(number)
6ff61434 836
e96e445b
MT
837 return ret
838
839 def set_phone_numbers(self, phone_numbers):
840 # Sort phone numbers by landline and mobile
841 _landline_numbers = []
842 _mobile_numbers = []
843
844 for number in phone_numbers:
845 try:
846 number = phonenumbers.parse(number, None)
847 except phonenumbers.phonenumberutil.NumberParseException:
848 continue
849
850 # Convert to string (in E.164 format)
851 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
852
853 # Separate mobile numbers
854 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
855 _mobile_numbers.append(s)
856 else:
857 _landline_numbers.append(s)
858
859 # Save
860 self._set_strings("telephoneNumber", _landline_numbers)
861 self._set_strings("mobile", _mobile_numbers)
862
863 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
864
865 @property
866 def _all_telephone_numbers(self):
6ccc8acb
MT
867 ret = [ self.sip_id, ]
868
d3208ac7
MT
869 if self.phone_number:
870 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
871 ret.append(s)
872
6ccc8acb
MT
873 for number in self.phone_numbers:
874 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
875 ret.append(s)
876
877 return ret
66862195 878
2cd9af74 879 def avatar_url(self, size=None):
2dac7110 880 url = "https://people.ipfire.org/users/%s.jpg" % self.uid
2cd9af74
MT
881
882 if size:
883 url += "?size=%s" % size
884
885 return url
886
2cd9af74 887 def get_avatar(self, size=None):
5ef115cd 888 photo = self._get_bytes("jpegPhoto")
2cd9af74 889
0109451c
MT
890 # Exit if no avatar is available
891 if not photo:
892 return
893
5ef115cd
MT
894 # Return the raw image if no size was requested
895 if size is None:
896 return photo
2cd9af74 897
5ef115cd 898 # Try to retrieve something from the cache
9c01e5ac 899 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
5ef115cd
MT
900 if avatar:
901 return avatar
1a226c83 902
5ef115cd 903 # Generate a new thumbnail
2de3dacc 904 avatar = util.generate_thumbnail(photo, size, square=True)
1a226c83 905
5ef115cd 906 # Save to cache for 15m
9c01e5ac 907 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
1a226c83 908
5ef115cd 909 return avatar
2cd9af74 910
5cc10421
MT
911 def upload_avatar(self, avatar):
912 self._set("jpegPhoto", avatar)
913
f4672785
MT
914 # SSH Keys
915
916 @lazy_property
917 def ssh_keys(self):
918 ret = []
919
920 for key in self._get_strings("sshPublicKey"):
921 s = sshpubkeys.SSHKey()
922
923 try:
924 s.parse(key)
925 except (sshpubkeys.InvalidKeyError, NotImplementedError) as e:
926 logging.warning("Could not parse SSH key %s: %s" % (key, e))
927 continue
928
929 ret.append(s)
930
931 return ret
60024cc8 932
44b75370 933 def get_ssh_key_by_hash_sha256(self, hash_sha256):
55b67ca4 934 for key in self.ssh_keys:
44b75370 935 if not key.hash_sha256() == hash_sha256:
55b67ca4
MT
936 continue
937
938 return key
939
0d1fb712
MT
940 def add_ssh_key(self, key):
941 k = sshpubkeys.SSHKey()
942
943 # Try to parse the key
944 k.parse(key)
945
946 # Check for types and sufficient sizes
947 if k.key_type == b"ssh-rsa":
948 if k.bits < 4096:
949 raise sshpubkeys.TooShortKeyError("RSA keys cannot be smaller than 4096 bits")
950
951 elif k.key_type == b"ssh-dss":
952 raise sshpubkeys.InvalidKeyError("DSA keys are not supported")
953
954 # Ignore any duplicates
955 if key in (k.keydata for k in self.ssh_keys):
956 logging.debug("SSH Key has already been added for %s: %s" % (self, key))
957 return
958
cc27cb63
MT
959 # Prepare transaction
960 modlist = []
961
962 # Add object class if user is not in it, yet
963 if not "ldapPublicKey" in self.classes:
964 modlist.append((ldap.MOD_ADD, "objectClass", b"ldapPublicKey"))
965
966 # Add key
967 modlist.append((ldap.MOD_ADD, "sshPublicKey", key.encode()))
968
0d1fb712 969 # Save key to LDAP
cc27cb63 970 self._modify(modlist)
0d1fb712
MT
971
972 # Append to cache
973 self.ssh_keys.append(k)
974
975 def delete_ssh_key(self, key):
976 if not key in (k.keydata for k in self.ssh_keys):
977 return
978
979 # Delete key from LDAP
cc27cb63
MT
980 if len(self.ssh_keys) > 1:
981 self._delete_string("sshPublicKey", key)
982 else:
983 self._modify([
984 (ldap.MOD_DELETE, "objectClass", b"ldapPublicKey"),
985 (ldap.MOD_DELETE, "sshPublicKey", key.encode()),
986 ])
0d1fb712 987
55b67ca4 988
23f84bbc
MT
989class StopForumSpam(Object):
990 def init(self, uid, email, address):
991 self.uid, self.email, self.address = uid, email, address
992
993 @tornado.gen.coroutine
994 def send_request(self, **kwargs):
995 arguments = {
996 "json" : "1",
997 }
998 arguments.update(kwargs)
999
1000 # Create request
1001 request = tornado.httpclient.HTTPRequest(
1002 "https://api.stopforumspam.org/api", method="POST")
1003 request.body = urllib.parse.urlencode(arguments)
1004
1005 # Send the request
1006 response = yield self.backend.http_client.fetch(request)
1007
1008 # Decode the JSON response
1009 return json.loads(response.body.decode())
1010
1011 @tornado.gen.coroutine
1012 def check_address(self):
1013 response = yield self.send_request(ip=self.address)
1014
1015 try:
1016 confidence = response["ip"]["confidence"]
1017 except KeyError:
1018 confidence = 100
1019
1020 logging.debug("Confidence for %s: %s" % (self.address, confidence))
1021
1022 return confidence
1023
1024 @tornado.gen.coroutine
1025 def check_username(self):
1026 response = yield self.send_request(username=self.uid)
1027
1028 try:
1029 confidence = response["username"]["confidence"]
1030 except KeyError:
1031 confidence = 100
1032
1033 logging.debug("Confidence for %s: %s" % (self.uid, confidence))
1034
1035 return confidence
1036
1037 @tornado.gen.coroutine
1038 def check_email(self):
1039 response = yield self.send_request(email=self.email)
1040
1041 try:
1042 confidence = response["email"]["confidence"]
1043 except KeyError:
1044 confidence = 100
1045
1046 logging.debug("Confidence for %s: %s" % (self.email, confidence))
1047
1048 return confidence
1049
1050 @tornado.gen.coroutine
1051 def check(self, threshold=95):
1052 """
1053 This function tries to detect if we have a spammer.
1054
1055 To honour the privacy of our users, we only send the IP
1056 address and username and if those are on the database, we
1057 will send the email address as well.
1058 """
1059 confidences = yield [self.check_address(), self.check_username()]
1060
1061 if any((c < threshold for c in confidences)):
1062 confidences += yield [self.check_email()]
1063
1064 # Build a score based on the lowest confidence
1065 return 100 - min(confidences)
1066
1067
940227cb
MT
1068if __name__ == "__main__":
1069 a = Accounts()
1070
11347e46 1071 print(a.list())