]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
fireinfo: Allow importing profiles for aarch64
[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",
d11a4f82 249 recipients=["moderators@ipfire.org"], account=account)
fd86345d
MT
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):
4b91a306
MT
289 session_id = util.random_string(64)
290
291 res = self.db.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
292 RETURNING session_id, time_expires", host, account.uid, session_id)
66862195
MT
293
294 # Session could not be created
295 if not res:
296 return None, None
297
298 logging.info("Created session %s for %s which expires %s" \
299 % (res.session_id, account, res.time_expires))
300 return res.session_id, res.time_expires
301
302 def destroy_session(self, session_id, host):
303 logging.info("Destroying session %s" % session_id)
304
305 self.db.execute("DELETE FROM sessions \
306 WHERE session_id = %s AND host = %s", session_id, host)
66862195
MT
307
308 def get_by_session(self, session_id, host):
309 logging.debug("Looking up session %s" % session_id)
310
311 res = self.db.get("SELECT uid FROM sessions WHERE session_id = %s \
312 AND host = %s AND NOW() BETWEEN time_created AND time_expires",
313 session_id, host)
314
315 # Session does not exist or has expired
316 if not res:
317 return
318
319 # Update the session expiration time
320 self.db.execute("UPDATE sessions SET time_expires = NOW() + INTERVAL '14 days' \
321 WHERE session_id = %s AND host = %s", session_id, host)
322
323 return self.get_by_uid(res.uid)
d86f6f18 324
8e69850a
MT
325 def cleanup(self):
326 # Cleanup expired sessions
327 self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
328
329 # Cleanup expired account activations
330 self.db.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
331
2dac7110
MT
332 # Discourse
333
334 def decode_discourse_payload(self, payload, signature):
335 # Check signature
336 calculated_signature = self.sign_discourse_payload(payload)
337
338 if not hmac.compare_digest(signature, calculated_signature):
339 raise ValueError("Invalid signature: %s" % signature)
340
341 # Decode the query string
342 qs = base64.b64decode(payload).decode()
343
344 # Parse the query string
345 data = {}
346 for key, val in urllib.parse.parse_qsl(qs):
347 data[key] = val
348
349 return data
350
351 def encode_discourse_payload(self, **args):
352 # Encode the arguments into an URL-formatted string
353 qs = urllib.parse.urlencode(args).encode()
354
355 # Encode into base64
356 return base64.b64encode(qs).decode()
357
358 def sign_discourse_payload(self, payload, secret=None):
359 if secret is None:
360 secret = self.settings.get("discourse_sso_secret")
361
362 # Calculate a HMAC using SHA256
363 h = hmac.new(secret.encode(),
364 msg=payload.encode(), digestmod="sha256")
365
366 return h.hexdigest()
367
940227cb 368
a6dc0bad 369class Account(Object):
66862195 370 def __init__(self, backend, dn, attrs=None):
a6dc0bad 371 Object.__init__(self, backend)
940227cb
MT
372 self.dn = dn
373
e96e445b 374 self.attributes = attrs or {}
940227cb 375
917434b8 376 def __str__(self):
d6e57f73
MT
377 if self.nickname:
378 return self.nickname
379
917434b8
MT
380 return self.name
381
940227cb
MT
382 def __repr__(self):
383 return "<%s %s>" % (self.__class__.__name__, self.dn)
384
541c952b
MT
385 def __eq__(self, other):
386 if isinstance(other, self.__class__):
387 return self.dn == other.dn
388
389 def __lt__(self, other):
390 if isinstance(other, self.__class__):
391 return self.name < other.name
940227cb
MT
392
393 @property
66862195
MT
394 def ldap(self):
395 return self.accounts.ldap
940227cb 396
e96e445b
MT
397 def _exists(self, key):
398 try:
399 self.attributes[key]
400 except KeyError:
401 return False
940227cb 402
e96e445b 403 return True
940227cb 404
e96e445b
MT
405 def _get(self, key):
406 for value in self.attributes.get(key, []):
407 yield value
940227cb 408
e96e445b
MT
409 def _get_bytes(self, key, default=None):
410 for value in self._get(key):
411 return value
412
413 return default
414
415 def _get_strings(self, key):
416 for value in self._get(key):
417 yield value.decode()
418
419 def _get_string(self, key, default=None):
420 for value in self._get_strings(key):
421 return value
422
423 return default
424
425 def _get_phone_numbers(self, key):
426 for value in self._get_strings(key):
427 yield phonenumbers.parse(value, None)
428
9150881e
MT
429 def _get_timestamp(self, key):
430 value = self._get_string(key)
431
432 # Parse the timestamp value and returns a datetime object
f345d70a
MT
433 if value:
434 return datetime.datetime.strptime(value, "%Y%m%d%H%M%SZ")
9150881e 435
e96e445b
MT
436 def _modify(self, modlist):
437 logging.debug("Modifying %s: %s" % (self.dn, modlist))
438
6e33e8e1
MT
439 # Authenticate before performing any write operations
440 self.accounts._authenticate()
441
e96e445b
MT
442 # Run modify operation
443 self.ldap.modify_s(self.dn, modlist)
444
91f72160 445 # Delete cached attributes
9c01e5ac 446 self.memcache.delete("accounts:%s:attrs" % self.dn)
91f72160 447
e96e445b
MT
448 def _set(self, key, values):
449 current = self._get(key)
450
451 # Don't do anything if nothing has changed
452 if list(current) == values:
453 return
454
455 # Remove all old values and add all new ones
456 modlist = []
940227cb 457
e96e445b
MT
458 if self._exists(key):
459 modlist.append((ldap.MOD_DELETE, key, None))
940227cb 460
e96e445b 461 # Add new values
47bb098f
MT
462 if values:
463 modlist.append((ldap.MOD_ADD, key, values))
e96e445b
MT
464
465 # Run modify operation
466 self._modify(modlist)
467
468 # Update cache
469 self.attributes.update({ key : values })
470
471 def _set_bytes(self, key, values):
472 return self._set(key, values)
473
474 def _set_strings(self, key, values):
47bb098f 475 return self._set(key, [e.encode() for e in values if e])
e96e445b
MT
476
477 def _set_string(self, key, value):
478 return self._set_strings(key, [value,])
940227cb 479
0d1fb712
MT
480 def _add(self, key, values):
481 modlist = [
482 (ldap.MOD_ADD, key, values),
483 ]
484
485 self._modify(modlist)
486
487 def _add_strings(self, key, values):
488 return self._add(key, [e.encode() for e in values])
489
490 def _add_string(self, key, value):
491 return self._add_strings(key, [value,])
492
493 def _delete(self, key, values):
494 modlist = [
495 (ldap.MOD_DELETE, key, values),
496 ]
497
498 self._modify(modlist)
499
500 def _delete_strings(self, key, values):
501 return self._delete(key, [e.encode() for e in values])
502
503 def _delete_string(self, key, value):
504 return self._delete_strings(key, [value,])
505
ddfa1d3d
MT
506 @lazy_property
507 def kerberos_attributes(self):
508 res = self.backend.accounts._query(
509 "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self.uid,
510 attrlist=[
511 "krbLastSuccessfulAuth",
512 "krbLastPasswordChange",
513 "krbLastFailedAuth",
514 "krbLoginFailedCount",
515 ],
516 limit=1,
517 search_base="cn=krb5,%s" % self.backend.accounts.search_base)
518
519 for dn, attrs in res:
520 return { key : attrs[key][0] for key in attrs }
521
522 return {}
523
524 @staticmethod
525 def _parse_date(s):
526 return datetime.datetime.strptime(s.decode(), "%Y%m%d%H%M%SZ")
527
528 @property
529 def last_successful_authentication(self):
530 try:
531 s = self.kerberos_attributes["krbLastSuccessfulAuth"]
532 except KeyError:
533 return None
534
535 return self._parse_date(s)
536
537 @property
538 def last_failed_authentication(self):
539 try:
540 s = self.kerberos_attributes["krbLastFailedAuth"]
541 except KeyError:
542 return None
543
544 return self._parse_date(s)
545
546 @property
547 def failed_login_count(self):
548 try:
549 count = self.kerberos_attributes["krbLoginFailedCount"].decode()
550 except KeyError:
551 return 0
552
553 try:
554 return int(count)
555 except ValueError:
556 return 0
557
6b582a4f 558 def passwd(self, password):
3ea97943
MT
559 """
560 Sets a new password
561 """
6b582a4f
MT
562 # The new password must have a score of 3 or better
563 quality = self.check_password_quality(password)
564 if quality["score"] < 3:
565 raise ValueError("Password too weak")
566
1babcd04 567 self.accounts._authenticate()
6b582a4f 568 self.ldap.passwd_s(self.dn, None, password)
3ea97943 569
940227cb
MT
570 def check_password(self, password):
571 """
572 Bind to the server with given credentials and return
573 true if password is corrent and false if not.
574
575 Raises exceptions from the server on any other errors.
576 """
0d1fb712
MT
577 if not password:
578 return
579
940227cb 580 logging.debug("Checking credentials for %s" % self.dn)
3ea97943
MT
581
582 # Create a new LDAP connection
583 ldap_uri = self.backend.settings.get("ldap_uri")
584 conn = ldap.initialize(ldap_uri)
585
940227cb 586 try:
3ea97943 587 conn.simple_bind_s(self.dn, password.encode("utf-8"))
940227cb 588 except ldap.INVALID_CREDENTIALS:
3ea97943 589 logging.debug("Account credentials are invalid for %s" % self)
940227cb
MT
590 return False
591
3ea97943
MT
592 logging.info("Successfully authenticated %s" % self)
593
940227cb
MT
594 return True
595
6b582a4f
MT
596 def check_password_quality(self, password):
597 """
598 Passwords are passed through zxcvbn to make sure
599 that they are strong enough.
600 """
601 return zxcvbn.zxcvbn(password, user_inputs=(
602 self.first_name, self.last_name,
603 ))
604
940227cb 605 def is_admin(self):
eae206f4 606 return self.is_member_of_group("sudo")
66862195 607
71a3109c 608 def is_staff(self):
eae206f4
MT
609 return self.is_member_of_group("staff")
610
611 def is_moderator(self):
612 return self.is_member_of_group("moderators")
71a3109c
MT
613
614 def has_shell(self):
615 return "posixAccount" in self.classes
616
617 def has_mail(self):
618 return "postfixMailUser" in self.classes
619
620 def has_sip(self):
621 return "sipUser" in self.classes or "sipRoutingObject" in self.classes
66862195 622
e96e445b
MT
623 def can_be_managed_by(self, account):
624 """
625 Returns True if account is allowed to manage this account
626 """
627 # Admins can manage all accounts
628 if account.is_admin():
629 return True
630
631 # Users can manage themselves
632 return self == account
633
66862195
MT
634 @property
635 def classes(self):
e96e445b 636 return self._get_strings("objectClass")
66862195
MT
637
638 @property
639 def uid(self):
e96e445b 640 return self._get_string("uid")
940227cb 641
a6dc0bad
MT
642 @property
643 def name(self):
e96e445b 644 return self._get_string("cn")
66862195 645
d6e57f73
MT
646 # Nickname
647
648 def get_nickname(self):
649 return self._get_string("displayName")
650
651 def set_nickname(self, nickname):
652 self._set_string("displayName", nickname)
653
654 nickname = property(get_nickname, set_nickname)
655
e96e445b
MT
656 # First Name
657
658 def get_first_name(self):
659 return self._get_string("givenName")
660
661 def set_first_name(self, first_name):
662 self._set_string("givenName", first_name)
663
664 # Update Common Name
665 self._set_string("cn", "%s %s" % (first_name, self.last_name))
666
667 first_name = property(get_first_name, set_first_name)
668
669 # Last Name
670
671 def get_last_name(self):
672 return self._get_string("sn")
673
674 def set_last_name(self, last_name):
675 self._set_string("sn", last_name)
676
677 # Update Common Name
678 self._set_string("cn", "%s %s" % (self.first_name, last_name))
679
680 last_name = property(get_last_name, set_last_name)
66862195 681
1bae74c7 682 @lazy_property
66862195 683 def groups(self):
819daf36 684 groups = self.memcache.get("accounts:%s:groups" % self.dn)
d8b04c72
MT
685 if not groups:
686 # Fetch groups from LDAP
687 groups = self._get_groups()
819daf36 688
d8b04c72
MT
689 # Cache groups for 5 min
690 self.memcache.set("accounts:%s:groups" % self.dn, groups, 300)
819daf36 691
d8b04c72 692 return sorted((Group(self.backend, gid) for gid in groups))
819daf36
MT
693
694 def _get_groups(self):
1bae74c7 695 groups = []
66862195 696
ae485256
MT
697 res = self.accounts._query("(| \
698 (&(objectClass=groupOfNames)(member=%s)) \
699 (&(objectClass=posixGroup)(memberUid=%s)) \
700 )" % (self.dn, self.uid), ["cn"])
66862195 701
1bae74c7
MT
702 for dn, attrs in res:
703 cns = attrs.get("cn")
704 if cns:
705 groups.append(cns[0].decode())
66862195 706
1bae74c7 707 return groups
66862195 708
eae206f4
MT
709 def is_member_of_group(self, gid):
710 """
711 Returns True if this account is a member of this group
712 """
713 return gid in (g.gid for g in self.groups)
714
9150881e
MT
715 # Created/Modified at
716
717 @property
718 def created_at(self):
719 return self._get_timestamp("createTimestamp")
720
721 @property
722 def modified_at(self):
723 return self._get_timestamp("modifyTimestamp")
724
e96e445b
MT
725 # Address
726
0099c2a7
MT
727 @property
728 def address(self):
729 address = []
730
731 if self.street:
732 address += self.street.splitlines()
733
734 if self.postal_code and self.city:
735 if self.country_code in ("AT", "DE"):
736 address.append("%s %s" % (self.postal_code, self.city))
737 else:
738 address.append("%s, %s" % (self.city, self.postal_code))
739 else:
740 address.append(self.city or self.postal_code)
741
742 if self.country_name:
743 address.append(self.country_name)
744
745 return address
746
747 def get_street(self):
748 return self._get_string("street") or self._get_string("homePostalAddress")
749
750 def set_street(self, street):
751 self._set_string("street", street)
e96e445b 752
0099c2a7 753 street = property(get_street, set_street)
66862195 754
0099c2a7
MT
755 def get_city(self):
756 return self._get_string("l") or ""
e96e445b 757
0099c2a7
MT
758 def set_city(self, city):
759 self._set_string("l", city)
e96e445b 760
0099c2a7 761 city = property(get_city, set_city)
e96e445b 762
0099c2a7
MT
763 def get_postal_code(self):
764 return self._get_string("postalCode") or ""
765
766 def set_postal_code(self, postal_code):
767 self._set_string("postalCode", postal_code)
768
769 postal_code = property(get_postal_code, set_postal_code)
770
771 # XXX This should be c
772 def get_country_code(self):
773 return self._get_string("st")
774
775 def set_country_code(self, country_code):
776 self._set_string("st", country_code)
777
778 country_code = property(get_country_code, set_country_code)
779
780 @property
781 def country_name(self):
782 if self.country_code:
783 return countries.get_name(self.country_code)
a6dc0bad 784
940227cb
MT
785 @property
786 def email(self):
d86f6f18 787 return self._get_string("mail")
940227cb 788
e96e445b
MT
789 # Mail Routing Address
790
791 def get_mail_routing_address(self):
792 return self._get_string("mailRoutingAddress", None)
793
794 def set_mail_routing_address(self, address):
47bb098f 795 self._set_string("mailRoutingAddress", address or None)
e96e445b
MT
796
797 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
798
66862195
MT
799 @property
800 def sip_id(self):
801 if "sipUser" in self.classes:
e96e445b 802 return self._get_string("sipAuthenticationUser")
66862195
MT
803
804 if "sipRoutingObject" in self.classes:
e96e445b 805 return self._get_string("sipLocalAddress")
66862195 806
2f51147a
MT
807 @property
808 def sip_password(self):
e96e445b
MT
809 return self._get_string("sipPassword")
810
811 @staticmethod
812 def _generate_sip_password():
813 return util.random_string(8)
2f51147a 814
66862195
MT
815 @property
816 def sip_url(self):
817 return "%s@ipfire.org" % self.sip_id
818
819 def uses_sip_forwarding(self):
e96e445b 820 if self.sip_routing_address:
66862195
MT
821 return True
822
823 return False
824
e96e445b
MT
825 # SIP Routing
826
827 def get_sip_routing_address(self):
66862195 828 if "sipRoutingObject" in self.classes:
e96e445b
MT
829 return self._get_string("sipRoutingAddress")
830
831 def set_sip_routing_address(self, address):
832 if not address:
833 address = None
834
835 # Don't do anything if nothing has changed
836 if self.get_sip_routing_address() == address:
837 return
838
839 if address:
79cce555
MT
840 # This is no longer a SIP user any more
841 try:
842 self._modify([
843 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
844 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
845 (ldap.MOD_DELETE, "sipPassword", None),
846 ])
847 except ldap.NO_SUCH_ATTRIBUTE:
848 pass
849
850 # Set new routing object
851 try:
852 self._modify([
853 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
854 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
855 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
856 ])
857
858 # If this is a change, we cannot add this again
859 except ldap.TYPE_OR_VALUE_EXISTS:
860 self._set_string("sipRoutingAddress", address)
e96e445b 861 else:
79cce555
MT
862 try:
863 self._modify([
864 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
865 (ldap.MOD_DELETE, "sipLocalAddress", None),
866 (ldap.MOD_DELETE, "sipRoutingAddress", None),
867 ])
868 except ldap.NO_SUCH_ATTRIBUTE:
869 pass
870
871 self._modify([
e96e445b
MT
872 (ldap.MOD_ADD, "objectClass", b"sipUser"),
873 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
874 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
79cce555 875 ])
e96e445b
MT
876
877 # XXX Cache is invalid here
878
879 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 880
917434b8
MT
881 @lazy_property
882 def sip_registrations(self):
883 sip_registrations = []
884
885 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
886 reg.account = self
887
888 sip_registrations.append(reg)
889
890 return sip_registrations
891
1f38be5a
MT
892 @lazy_property
893 def sip_channels(self):
894 return self.backend.talk.freeswitch.get_sip_channels(self)
895
bdaf6b46
MT
896 def get_cdr(self, date=None, limit=None):
897 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 898
e96e445b 899 # Phone Numbers
6ff61434 900
d3208ac7
MT
901 @lazy_property
902 def phone_number(self):
903 """
904 Returns the IPFire phone number
905 """
906 if self.sip_id:
907 return phonenumbers.parse("+4923636035%s" % self.sip_id)
908
909 @lazy_property
910 def fax_number(self):
911 if self.sip_id:
912 return phonenumbers.parse("+49236360359%s" % self.sip_id)
913
e96e445b
MT
914 def get_phone_numbers(self):
915 ret = []
6ff61434 916
e96e445b
MT
917 for field in ("telephoneNumber", "homePhone", "mobile"):
918 for number in self._get_phone_numbers(field):
919 ret.append(number)
6ff61434 920
e96e445b
MT
921 return ret
922
923 def set_phone_numbers(self, phone_numbers):
924 # Sort phone numbers by landline and mobile
925 _landline_numbers = []
926 _mobile_numbers = []
927
928 for number in phone_numbers:
929 try:
930 number = phonenumbers.parse(number, None)
931 except phonenumbers.phonenumberutil.NumberParseException:
932 continue
933
934 # Convert to string (in E.164 format)
935 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
936
937 # Separate mobile numbers
938 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
939 _mobile_numbers.append(s)
940 else:
941 _landline_numbers.append(s)
942
943 # Save
944 self._set_strings("telephoneNumber", _landline_numbers)
945 self._set_strings("mobile", _mobile_numbers)
946
947 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
948
949 @property
950 def _all_telephone_numbers(self):
6ccc8acb
MT
951 ret = [ self.sip_id, ]
952
d3208ac7
MT
953 if self.phone_number:
954 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
955 ret.append(s)
956
6ccc8acb
MT
957 for number in self.phone_numbers:
958 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
959 ret.append(s)
960
961 return ret
66862195 962
5d42f49b
MT
963 def has_avatar(self):
964 has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
965 if has_avatar is None:
966 has_avatar = True if self.get_avatar() else False
967
968 # Cache avatar status for up to 24 hours
969 self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
970
971 return has_avatar
972
2cd9af74 973 def avatar_url(self, size=None):
2dac7110 974 url = "https://people.ipfire.org/users/%s.jpg" % self.uid
2cd9af74
MT
975
976 if size:
977 url += "?size=%s" % size
978
979 return url
980
2cd9af74 981 def get_avatar(self, size=None):
5ef115cd 982 photo = self._get_bytes("jpegPhoto")
2cd9af74 983
0109451c
MT
984 # Exit if no avatar is available
985 if not photo:
986 return
987
5ef115cd
MT
988 # Return the raw image if no size was requested
989 if size is None:
990 return photo
2cd9af74 991
5ef115cd 992 # Try to retrieve something from the cache
9c01e5ac 993 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
5ef115cd
MT
994 if avatar:
995 return avatar
1a226c83 996
5ef115cd 997 # Generate a new thumbnail
2de3dacc 998 avatar = util.generate_thumbnail(photo, size, square=True)
1a226c83 999
5ef115cd 1000 # Save to cache for 15m
9c01e5ac 1001 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
1a226c83 1002
5ef115cd 1003 return avatar
2cd9af74 1004
5cc10421
MT
1005 def upload_avatar(self, avatar):
1006 self._set("jpegPhoto", avatar)
1007
5d42f49b
MT
1008 # Delete cached avatar status
1009 self.memcache.delete("accounts:%s:has-avatar" % self.uid)
1010
55b67ca4 1011
23f84bbc
MT
1012class StopForumSpam(Object):
1013 def init(self, uid, email, address):
1014 self.uid, self.email, self.address = uid, email, address
1015
1016 @tornado.gen.coroutine
1017 def send_request(self, **kwargs):
1018 arguments = {
1019 "json" : "1",
1020 }
1021 arguments.update(kwargs)
1022
1023 # Create request
1024 request = tornado.httpclient.HTTPRequest(
1025 "https://api.stopforumspam.org/api", method="POST")
1026 request.body = urllib.parse.urlencode(arguments)
1027
1028 # Send the request
1029 response = yield self.backend.http_client.fetch(request)
1030
1031 # Decode the JSON response
1032 return json.loads(response.body.decode())
1033
1034 @tornado.gen.coroutine
1035 def check_address(self):
1036 response = yield self.send_request(ip=self.address)
1037
1038 try:
1039 confidence = response["ip"]["confidence"]
1040 except KeyError:
1041 confidence = 100
1042
1043 logging.debug("Confidence for %s: %s" % (self.address, confidence))
1044
1045 return confidence
1046
1047 @tornado.gen.coroutine
1048 def check_username(self):
1049 response = yield self.send_request(username=self.uid)
1050
1051 try:
1052 confidence = response["username"]["confidence"]
1053 except KeyError:
1054 confidence = 100
1055
1056 logging.debug("Confidence for %s: %s" % (self.uid, confidence))
1057
1058 return confidence
1059
1060 @tornado.gen.coroutine
1061 def check_email(self):
1062 response = yield self.send_request(email=self.email)
1063
1064 try:
1065 confidence = response["email"]["confidence"]
1066 except KeyError:
1067 confidence = 100
1068
1069 logging.debug("Confidence for %s: %s" % (self.email, confidence))
1070
1071 return confidence
1072
1073 @tornado.gen.coroutine
1074 def check(self, threshold=95):
1075 """
1076 This function tries to detect if we have a spammer.
1077
1078 To honour the privacy of our users, we only send the IP
1079 address and username and if those are on the database, we
1080 will send the email address as well.
1081 """
1082 confidences = yield [self.check_address(), self.check_username()]
1083
1084 if any((c < threshold for c in confidences)):
1085 confidences += yield [self.check_email()]
1086
1087 # Build a score based on the lowest confidence
1088 return 100 - min(confidences)
1089
1090
d8b04c72
MT
1091
1092class Groups(Object):
1093 @property
1094 def search_base(self):
1095 return "ou=Group,%s" % self.backend.accounts.search_base
1096
1097
1098class Group(Object):
1099 def init(self, gid):
1100 self.gid = gid
1101
1102 def __repr__(self):
1103 if self.description:
1104 return "<%s %s (%s)>" % (
1105 self.__class__.__name__,
1106 self.gid,
1107 self.description,
1108 )
1109
1110 return "<%s %s>" % (self.__class__.__name__, self.gid)
1111
1112 def __str__(self):
1113 return self.description or self.gid
1114
1115 def __eq__(self, other):
1116 if isinstance(other, self.__class__):
1117 return self.gid == other.gid
1118
1119 def __lt__(self, other):
1120 if isinstance(other, self.__class__):
1121 return (self.description or self.gid) < (other.description or other.gid)
1122
1123 @lazy_property
1124 def attributes(self):
1125 attrs = self.memcache.get("groups:%s:attrs" % self.gid)
1126 if not attrs:
1127 attrs = self._get_attrs()
1128
1129 # Cache this for 5 mins
1130 self.memcache.set("groups:%s:attrs" % self.gid, attrs, 300)
1131
1132 return attrs
1133
1134 def _get_attrs(self):
1135 res = self.backend.accounts._query(
1136 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % self.gid,
1137 search_base=self.backend.groups.search_base, limit=1)
1138
1139 for dn, attrs in res:
1140 return attrs
1141
1142 @property
1143 def description(self):
1144 try:
1145 description = self.attributes["description"][0]
1146 except KeyError:
1147 return None
1148
1149 return description.decode()
1150
1151
940227cb
MT
1152if __name__ == "__main__":
1153 a = Accounts()
1154
11347e46 1155 print(a.list())