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