]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/accounts.py
people: Add groups to navigation
[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):
18b13823 684 return self.backend.groups._get_groups("(| \
ae485256
MT
685 (&(objectClass=groupOfNames)(member=%s)) \
686 (&(objectClass=posixGroup)(memberUid=%s)) \
18b13823 687 )" % (self.dn, self.uid))
66862195 688
eae206f4
MT
689 def is_member_of_group(self, gid):
690 """
691 Returns True if this account is a member of this group
692 """
693 return gid in (g.gid for g in self.groups)
694
9150881e
MT
695 # Created/Modified at
696
697 @property
698 def created_at(self):
699 return self._get_timestamp("createTimestamp")
700
701 @property
702 def modified_at(self):
703 return self._get_timestamp("modifyTimestamp")
704
e96e445b
MT
705 # Address
706
0099c2a7
MT
707 @property
708 def address(self):
709 address = []
710
711 if self.street:
712 address += self.street.splitlines()
713
714 if self.postal_code and self.city:
715 if self.country_code in ("AT", "DE"):
716 address.append("%s %s" % (self.postal_code, self.city))
717 else:
718 address.append("%s, %s" % (self.city, self.postal_code))
719 else:
720 address.append(self.city or self.postal_code)
721
722 if self.country_name:
723 address.append(self.country_name)
724
725 return address
726
727 def get_street(self):
728 return self._get_string("street") or self._get_string("homePostalAddress")
729
730 def set_street(self, street):
731 self._set_string("street", street)
e96e445b 732
0099c2a7 733 street = property(get_street, set_street)
66862195 734
0099c2a7
MT
735 def get_city(self):
736 return self._get_string("l") or ""
e96e445b 737
0099c2a7
MT
738 def set_city(self, city):
739 self._set_string("l", city)
e96e445b 740
0099c2a7 741 city = property(get_city, set_city)
e96e445b 742
0099c2a7
MT
743 def get_postal_code(self):
744 return self._get_string("postalCode") or ""
745
746 def set_postal_code(self, postal_code):
747 self._set_string("postalCode", postal_code)
748
749 postal_code = property(get_postal_code, set_postal_code)
750
751 # XXX This should be c
752 def get_country_code(self):
753 return self._get_string("st")
754
755 def set_country_code(self, country_code):
756 self._set_string("st", country_code)
757
758 country_code = property(get_country_code, set_country_code)
759
760 @property
761 def country_name(self):
762 if self.country_code:
763 return countries.get_name(self.country_code)
a6dc0bad 764
940227cb
MT
765 @property
766 def email(self):
d86f6f18 767 return self._get_string("mail")
940227cb 768
e96e445b
MT
769 # Mail Routing Address
770
771 def get_mail_routing_address(self):
772 return self._get_string("mailRoutingAddress", None)
773
774 def set_mail_routing_address(self, address):
47bb098f 775 self._set_string("mailRoutingAddress", address or None)
e96e445b
MT
776
777 mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
778
66862195
MT
779 @property
780 def sip_id(self):
781 if "sipUser" in self.classes:
e96e445b 782 return self._get_string("sipAuthenticationUser")
66862195
MT
783
784 if "sipRoutingObject" in self.classes:
e96e445b 785 return self._get_string("sipLocalAddress")
66862195 786
2f51147a
MT
787 @property
788 def sip_password(self):
e96e445b
MT
789 return self._get_string("sipPassword")
790
791 @staticmethod
792 def _generate_sip_password():
793 return util.random_string(8)
2f51147a 794
66862195
MT
795 @property
796 def sip_url(self):
797 return "%s@ipfire.org" % self.sip_id
798
799 def uses_sip_forwarding(self):
e96e445b 800 if self.sip_routing_address:
66862195
MT
801 return True
802
803 return False
804
e96e445b
MT
805 # SIP Routing
806
807 def get_sip_routing_address(self):
66862195 808 if "sipRoutingObject" in self.classes:
e96e445b
MT
809 return self._get_string("sipRoutingAddress")
810
811 def set_sip_routing_address(self, address):
812 if not address:
813 address = None
814
815 # Don't do anything if nothing has changed
816 if self.get_sip_routing_address() == address:
817 return
818
819 if address:
79cce555
MT
820 # This is no longer a SIP user any more
821 try:
822 self._modify([
823 (ldap.MOD_DELETE, "objectClass", b"sipUser"),
824 (ldap.MOD_DELETE, "sipAuthenticationUser", None),
825 (ldap.MOD_DELETE, "sipPassword", None),
826 ])
827 except ldap.NO_SUCH_ATTRIBUTE:
828 pass
829
830 # Set new routing object
831 try:
832 self._modify([
833 (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
834 (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
835 (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
836 ])
837
838 # If this is a change, we cannot add this again
839 except ldap.TYPE_OR_VALUE_EXISTS:
840 self._set_string("sipRoutingAddress", address)
e96e445b 841 else:
79cce555
MT
842 try:
843 self._modify([
844 (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
845 (ldap.MOD_DELETE, "sipLocalAddress", None),
846 (ldap.MOD_DELETE, "sipRoutingAddress", None),
847 ])
848 except ldap.NO_SUCH_ATTRIBUTE:
849 pass
850
851 self._modify([
e96e445b
MT
852 (ldap.MOD_ADD, "objectClass", b"sipUser"),
853 (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
854 (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
79cce555 855 ])
e96e445b
MT
856
857 # XXX Cache is invalid here
858
859 sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
66862195 860
917434b8
MT
861 @lazy_property
862 def sip_registrations(self):
863 sip_registrations = []
864
865 for reg in self.backend.talk.freeswitch.get_sip_registrations(self.sip_url):
866 reg.account = self
867
868 sip_registrations.append(reg)
869
870 return sip_registrations
871
1f38be5a
MT
872 @lazy_property
873 def sip_channels(self):
874 return self.backend.talk.freeswitch.get_sip_channels(self)
875
bdaf6b46
MT
876 def get_cdr(self, date=None, limit=None):
877 return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
525c01f7 878
e96e445b 879 # Phone Numbers
6ff61434 880
d3208ac7
MT
881 @lazy_property
882 def phone_number(self):
883 """
884 Returns the IPFire phone number
885 """
886 if self.sip_id:
887 return phonenumbers.parse("+4923636035%s" % self.sip_id)
888
889 @lazy_property
890 def fax_number(self):
891 if self.sip_id:
892 return phonenumbers.parse("+49236360359%s" % self.sip_id)
893
e96e445b
MT
894 def get_phone_numbers(self):
895 ret = []
6ff61434 896
e96e445b
MT
897 for field in ("telephoneNumber", "homePhone", "mobile"):
898 for number in self._get_phone_numbers(field):
899 ret.append(number)
6ff61434 900
e96e445b
MT
901 return ret
902
903 def set_phone_numbers(self, phone_numbers):
904 # Sort phone numbers by landline and mobile
905 _landline_numbers = []
906 _mobile_numbers = []
907
908 for number in phone_numbers:
909 try:
910 number = phonenumbers.parse(number, None)
911 except phonenumbers.phonenumberutil.NumberParseException:
912 continue
913
914 # Convert to string (in E.164 format)
915 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
916
917 # Separate mobile numbers
918 if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
919 _mobile_numbers.append(s)
920 else:
921 _landline_numbers.append(s)
922
923 # Save
924 self._set_strings("telephoneNumber", _landline_numbers)
925 self._set_strings("mobile", _mobile_numbers)
926
927 phone_numbers = property(get_phone_numbers, set_phone_numbers)
8476e80f
MT
928
929 @property
930 def _all_telephone_numbers(self):
6ccc8acb
MT
931 ret = [ self.sip_id, ]
932
d3208ac7
MT
933 if self.phone_number:
934 s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
935 ret.append(s)
936
6ccc8acb
MT
937 for number in self.phone_numbers:
938 s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
939 ret.append(s)
940
941 return ret
66862195 942
5d42f49b
MT
943 def has_avatar(self):
944 has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
945 if has_avatar is None:
946 has_avatar = True if self.get_avatar() else False
947
948 # Cache avatar status for up to 24 hours
949 self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
950
951 return has_avatar
952
2cd9af74 953 def avatar_url(self, size=None):
2dac7110 954 url = "https://people.ipfire.org/users/%s.jpg" % self.uid
2cd9af74
MT
955
956 if size:
957 url += "?size=%s" % size
958
959 return url
960
2cd9af74 961 def get_avatar(self, size=None):
5ef115cd 962 photo = self._get_bytes("jpegPhoto")
2cd9af74 963
0109451c
MT
964 # Exit if no avatar is available
965 if not photo:
966 return
967
5ef115cd
MT
968 # Return the raw image if no size was requested
969 if size is None:
970 return photo
2cd9af74 971
5ef115cd 972 # Try to retrieve something from the cache
9c01e5ac 973 avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
5ef115cd
MT
974 if avatar:
975 return avatar
1a226c83 976
5ef115cd 977 # Generate a new thumbnail
2de3dacc 978 avatar = util.generate_thumbnail(photo, size, square=True)
1a226c83 979
5ef115cd 980 # Save to cache for 15m
9c01e5ac 981 self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
1a226c83 982
5ef115cd 983 return avatar
2cd9af74 984
5cc10421
MT
985 def upload_avatar(self, avatar):
986 self._set("jpegPhoto", avatar)
987
5d42f49b
MT
988 # Delete cached avatar status
989 self.memcache.delete("accounts:%s:has-avatar" % self.uid)
990
55b67ca4 991
23f84bbc
MT
992class StopForumSpam(Object):
993 def init(self, uid, email, address):
994 self.uid, self.email, self.address = uid, email, address
995
996 @tornado.gen.coroutine
997 def send_request(self, **kwargs):
998 arguments = {
999 "json" : "1",
1000 }
1001 arguments.update(kwargs)
1002
1003 # Create request
1004 request = tornado.httpclient.HTTPRequest(
1005 "https://api.stopforumspam.org/api", method="POST")
1006 request.body = urllib.parse.urlencode(arguments)
1007
1008 # Send the request
1009 response = yield self.backend.http_client.fetch(request)
1010
1011 # Decode the JSON response
1012 return json.loads(response.body.decode())
1013
1014 @tornado.gen.coroutine
1015 def check_address(self):
1016 response = yield self.send_request(ip=self.address)
1017
1018 try:
1019 confidence = response["ip"]["confidence"]
1020 except KeyError:
1021 confidence = 100
1022
1023 logging.debug("Confidence for %s: %s" % (self.address, confidence))
1024
1025 return confidence
1026
1027 @tornado.gen.coroutine
1028 def check_username(self):
1029 response = yield self.send_request(username=self.uid)
1030
1031 try:
1032 confidence = response["username"]["confidence"]
1033 except KeyError:
1034 confidence = 100
1035
1036 logging.debug("Confidence for %s: %s" % (self.uid, confidence))
1037
1038 return confidence
1039
1040 @tornado.gen.coroutine
1041 def check_email(self):
1042 response = yield self.send_request(email=self.email)
1043
1044 try:
1045 confidence = response["email"]["confidence"]
1046 except KeyError:
1047 confidence = 100
1048
1049 logging.debug("Confidence for %s: %s" % (self.email, confidence))
1050
1051 return confidence
1052
1053 @tornado.gen.coroutine
1054 def check(self, threshold=95):
1055 """
1056 This function tries to detect if we have a spammer.
1057
1058 To honour the privacy of our users, we only send the IP
1059 address and username and if those are on the database, we
1060 will send the email address as well.
1061 """
1062 confidences = yield [self.check_address(), self.check_username()]
1063
1064 if any((c < threshold for c in confidences)):
1065 confidences += yield [self.check_email()]
1066
1067 # Build a score based on the lowest confidence
1068 return 100 - min(confidences)
1069
1070
d8b04c72 1071class Groups(Object):
b6365721
MT
1072 hidden_groups = (
1073 "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
1074 "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
d71fd279
MT
1075 "cn=sudo,ou=Group,dc=ipfire,dc=org",
1076
1077 # Everyone is a member of people
1078 "cn=people,ou=Group,dc=ipfire,dc=org",
b6365721
MT
1079 )
1080
d8b04c72
MT
1081 @property
1082 def search_base(self):
1083 return "ou=Group,%s" % self.backend.accounts.search_base
1084
18b13823
MT
1085 def _query(self, *args, **kwargs):
1086 kwargs.update({
1087 "search_base" : self.backend.groups.search_base,
1088 })
1089
1090 return self.backend.accounts._query(*args, **kwargs)
1091
1092 def __iter__(self):
1093 groups = self.get_all()
1094
1095 return iter(groups)
1096
1097 def _get_groups(self, query, **kwargs):
1098 res = self._query(query, **kwargs)
1099
1100 groups = []
1101 for dn, attrs in res:
b6365721
MT
1102 # Skip any hidden groups
1103 if dn in self.hidden_groups:
1104 continue
1105
18b13823
MT
1106 g = Group(self.backend, dn, attrs)
1107 groups.append(g)
1108
1109 return sorted(groups)
1110
bef47ee8
MT
1111 def _get_group(self, query, **kwargs):
1112 kwargs.update({
1113 "limit" : 1,
1114 })
1115
1116 groups = self._get_groups(query, **kwargs)
1117 if groups:
1118 return groups[0]
1119
18b13823
MT
1120 def get_all(self):
1121 return self._get_groups(
1122 "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
1123 )
1124
bef47ee8
MT
1125 def get_by_gid(self, gid):
1126 return self._get_group(
1127 "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid,
1128 )
1129
d8b04c72
MT
1130
1131class Group(Object):
18b13823
MT
1132 def init(self, dn, attrs=None):
1133 self.dn = dn
1134
1135 self.attributes = attrs or {}
d8b04c72
MT
1136
1137 def __repr__(self):
1138 if self.description:
1139 return "<%s %s (%s)>" % (
1140 self.__class__.__name__,
1141 self.gid,
1142 self.description,
1143 )
1144
1145 return "<%s %s>" % (self.__class__.__name__, self.gid)
1146
1147 def __str__(self):
1148 return self.description or self.gid
1149
1150 def __eq__(self, other):
1151 if isinstance(other, self.__class__):
1152 return self.gid == other.gid
1153
1154 def __lt__(self, other):
1155 if isinstance(other, self.__class__):
1156 return (self.description or self.gid) < (other.description or other.gid)
1157
d71fd279
MT
1158 def __bool__(self):
1159 return True
1160
74f967de
MT
1161 def __len__(self):
1162 """
1163 Returns the number of members in this group
1164 """
1165 l = 0
1166
1167 for attr in ("member", "memberUid"):
1168 a = self.attributes.get(attr, None)
1169 if a:
1170 l += len(a)
1171
1172 return l
1173
bef47ee8
MT
1174 def __iter__(self):
1175 return iter(self.members)
1176
18b13823
MT
1177 @property
1178 def gid(self):
1179 try:
1180 gid = self.attributes["cn"][0]
1181 except KeyError:
1182 return None
d8b04c72 1183
18b13823 1184 return gid.decode()
d8b04c72
MT
1185
1186 @property
1187 def description(self):
1188 try:
1189 description = self.attributes["description"][0]
1190 except KeyError:
1191 return None
1192
1193 return description.decode()
1194
83a4b1d5
MT
1195 @property
1196 def email(self):
1197 try:
1198 email = self.attributes["mail"][0]
1199 except KeyError:
1200 return None
1201
1202 return email.decode()
1203
bef47ee8
MT
1204 @lazy_property
1205 def members(self):
1206 members = []
1207
1208 # Get all members by DN
1209 for dn in self.attributes.get("member", []):
1210 member = self.backend.accounts.get_by_dn(dn.decode())
1211 if member:
1212 members.append(member)
1213
1214 # Get all meembers by UID
1215 for uid in self.attributes.get("memberUid", []):
1216 member = self.backend.accounts.get_by_uid(uid.decode())
1217 if member:
1218 members.append(member)
1219
1220 return sorted(members)
d8b04c72 1221
940227cb
MT
1222if __name__ == "__main__":
1223 a = Accounts()
1224
11347e46 1225 print(a.list())