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