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