#!/usr/bin/python
# encoding: utf-8
-import PIL
-import io
+import base64
+import datetime
+import hmac
+import json
import ldap
+import ldap.modlist
import logging
+import os
+import phonenumbers
+import re
+import time
+import tornado.httpclient
import urllib.parse
import urllib.request
+import zxcvbn
+from . import countries
+from . import util
from .decorators import *
from .misc import Object
+# Set the client keytab name
+os.environ["KRB5_CLIENT_KTNAME"] = "/etc/ipfire.org/ldap.keytab"
+
class Accounts(Object):
- @property
+ def init(self):
+ self.search_base = self.settings.get("ldap_search_base")
+
+ def __iter__(self):
+ # Only return developers (group with ID 1000)
+ accounts = self._search("(&(objectClass=posixAccount)(gidNumber=1000))")
+
+ return iter(sorted(accounts))
+
+ @lazy_property
def ldap(self):
- if not hasattr(self, "_ldap"):
- # Connect to LDAP server
- ldap_uri = self.settings.get("ldap_uri")
- self._ldap = ldap.initialize(ldap_uri)
+ # Connect to LDAP server
+ ldap_uri = self.settings.get("ldap_uri")
- # Bind with username and password
- bind_dn = self.settings.get("ldap_bind_dn")
- if bind_dn:
- bind_pw = self.settings.get("ldap_bind_pw", "")
- self._ldap.simple_bind(bind_dn, bind_pw)
+ logging.debug("Connecting to LDAP server: %s" % ldap_uri)
- return self._ldap
+ # Connect to the LDAP server
+ return ldap.ldapobject.ReconnectLDAPObject(ldap_uri,
+ retry_max=10, retry_delay=3)
- def _search(self, query, attrlist=None, limit=0):
- logging.debug("Performing LDAP query: %s" % query)
+ def _authenticate(self):
+ # Authenticate against LDAP server using Kerberos
+ self.ldap.sasl_gssapi_bind_s()
- search_base = self.settings.get("ldap_search_base")
+ def test_ldap(self):
+ logging.info("Testing LDAP connection...")
- try:
- results = self.ldap.search_ext_s(search_base, ldap.SCOPE_SUBTREE,
- query, attrlist=attrlist, sizelimit=limit)
- except:
- # Close current connection
- del self._ldap
+ self._authenticate()
- raise
+ logging.info("Successfully authenticated as %s" % self.ldap.whoami_s())
- return results
+ def _query(self, query, attrlist=None, limit=0, search_base=None):
+ logging.debug("Performing LDAP query (%s): %s" \
+ % (search_base or self.search_base, query))
+
+ t = time.time()
- def search(self, query, limit=0):
- results = self._search(query, limit=limit)
+ results = self.ldap.search_ext_s(search_base or self.search_base,
+ ldap.SCOPE_SUBTREE, query, attrlist=attrlist, sizelimit=limit)
+ # Log time it took to perform the query
+ logging.debug("Query took %.2fms" % ((time.time() - t) * 1000.0))
+
+ return results
+
+ def _search(self, query, attrlist=None, limit=0):
accounts = []
- for dn, attrs in results:
- account = Account(self.backend, dn, attrs)
+ for dn, attrs in self._query(query, attrlist=["dn"], limit=limit):
+ account = self.get_by_dn(dn)
accounts.append(account)
+ return accounts
+
+ def _get_attrs(self, dn):
+ """
+ Fetches all attributes for the given distinguished name
+ """
+ results = self._query("(objectClass=*)", search_base=dn, limit=1,
+ attrlist=("*", "createTimestamp", "modifyTimestamp"))
+
+ for dn, attrs in results:
+ return attrs
+
+ def get_by_dn(self, dn):
+ attrs = self.memcache.get("accounts:%s:attrs" % dn)
+ if attrs is None:
+ attrs = self._get_attrs(dn)
+ assert attrs, dn
+
+ # Cache all attributes for 5 min
+ self.memcache.set("accounts:%s:attrs" % dn, attrs, 300)
+
+ return Account(self.backend, dn, attrs)
+
+ def get_created_after(self, ts):
+ t = ts.strftime("%Y%m%d%H%M%SZ")
+
+ return self._search("(&(objectClass=person)(createTimestamp>=%s))" % t)
+
+ def search(self, query):
+ # Search for exact matches
+ accounts = self._search(
+ "(&(objectClass=person)(|(uid=%s)(mail=%s)(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
+ % (query, query, query, query, query, query))
+
+ # Find accounts by name
+ if not accounts:
+ for account in self._search("(&(objectClass=person)(|(cn=*%s*)(uid=*%s*)))" % (query, query)):
+ if not account in accounts:
+ accounts.append(account)
+
return sorted(accounts)
- def search_one(self, query):
- result = self.search(query, limit=1)
- assert len(result) <= 1
+ def _search_one(self, query):
+ results = self._search(query, limit=1)
- if result:
- return result[0]
+ for result in results:
+ return result
- def get_all(self):
- # Only return developers (group with ID 1000)
- return self.search("(&(objectClass=posixAccount)(gidNumber=1000))")
+ def uid_is_valid(self, uid):
+ # UID must be at least four characters
+ if len(uid) <= 4:
+ return False
+
+ # https://unix.stackexchange.com/questions/157426/what-is-the-regex-to-validate-linux-users
+ m = re.match(r"^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$", uid)
+ if m:
+ return True
+
+ return False
+
+ def uid_exists(self, uid):
+ if self.get_by_uid(uid):
+ return True
+
+ res = self.db.get("SELECT 1 FROM account_activations \
+ WHERE uid = %s AND expires_at > NOW()", uid)
+
+ if res:
+ return True
- list = get_all
+ # Account with uid does not exist, yet
+ return False
def get_by_uid(self, uid):
- return self.search_one("(&(objectClass=posixAccount)(uid=%s))" % uid)
+ return self._search_one("(&(objectClass=person)(uid=%s))" % uid)
def get_by_mail(self, mail):
- return self.search_one("(&(objectClass=posixAccount)(mail=%s))" % mail)
-
- find = get_by_uid
+ return self._search_one("(&(objectClass=inetOrgPerson)(mail=%s))" % mail)
def find_account(self, s):
account = self.get_by_uid(s)
return self.get_by_mail(s)
def get_by_sip_id(self, sip_id):
- return self.search_one("(|(&(objectClass=sipUser)(sipAuthenticationUser=%s)) \
- (&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" % (sip_id, sip_id))
+ if not sip_id:
+ return
- # Session stuff
+ return self._search_one(
+ "(|(&(objectClass=sipUser)(sipAuthenticationUser=%s))(&(objectClass=sipRoutingObject)(sipLocalAddress=%s)))" \
+ % (sip_id, sip_id))
- def _cleanup_expired_sessions(self):
- self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
+ def get_by_phone_number(self, number):
+ if not number:
+ return
+
+ return self._search_one(
+ "(&(objectClass=inetOrgPerson)(|(sipAuthenticationUser=%s)(telephoneNumber=%s)(homePhone=%s)(mobile=%s)))" \
+ % (number, number, number, number))
+
+ @tornado.gen.coroutine
+ def check_spam(self, uid, email, address):
+ sfs = StopForumSpam(self.backend, uid, email, address)
+
+ # Get spam score
+ score = yield sfs.check()
+
+ return score >= 50
+
+ def auth(self, username, password):
+ # Find account
+ account = self.backend.accounts.find_account(username)
+
+ # Check credentials
+ if account and account.check_password(password):
+ return account
+
+ # Registration
+
+ def register(self, uid, email, first_name, last_name, country_code=None):
+ # Convert all uids to lowercase
+ uid = uid.lower()
+
+ # Check if UID is valid
+ if not self.uid_is_valid(uid):
+ raise ValueError("UID is invalid: %s" % uid)
+
+ # Check if UID is unique
+ if self.uid_exists(uid):
+ raise ValueError("UID exists: %s" % uid)
+
+ # Generate a random activation code
+ activation_code = util.random_string(36)
+
+ # Create an entry in our database until the user
+ # has activated the account
+ self.db.execute("INSERT INTO account_activations(uid, activation_code, \
+ email, first_name, last_name, country_code) VALUES(%s, %s, %s, %s, %s, %s)",
+ uid, activation_code, email, first_name, last_name, country_code)
+
+ # Send an account activation email
+ self.backend.messages.send_template("auth/messages/register",
+ recipients=[email], priority=100, uid=uid,
+ activation_code=activation_code, email=email,
+ first_name=first_name, last_name=last_name)
+
+ def activate(self, uid, activation_code):
+ res = self.db.get("DELETE FROM account_activations \
+ WHERE uid = %s AND activation_code = %s AND expires_at > NOW() \
+ RETURNING *", uid, activation_code)
+
+ # Return nothing when account was not found
+ if not res:
+ return
+
+ # Return the account if it has already been created
+ account = self.get_by_uid(uid)
+ if account:
+ return account
+
+ # Create a new account on the LDAP database
+ account = self.create(uid, res.email,
+ first_name=res.first_name, last_name=res.last_name,
+ country_code=res.country_code)
+
+ # Send email about account registration
+ self.backend.messages.send_template("people/messages/new-account",
+ recipients=["moderators@ipfire.org"], account=account)
+
+ return account
+
+ def create(self, uid, email, first_name, last_name, country_code=None):
+ cn = "%s %s" % (first_name, last_name)
+
+ # Account Parameters
+ account = {
+ "objectClass" : [b"top", b"person", b"inetOrgPerson"],
+ "mail" : email.encode(),
+
+ # Name
+ "cn" : cn.encode(),
+ "sn" : last_name.encode(),
+ "givenName" : first_name.encode(),
+ }
+
+ logging.info("Creating new account: %s: %s" % (uid, account))
+
+ # Create DN
+ dn = "uid=%s,ou=People,dc=ipfire,dc=org" % uid
+
+ # Create account on LDAP
+ self.accounts._authenticate()
+ self.ldap.add_s(dn, ldap.modlist.addModlist(account))
+
+ # Fetch the account
+ account = self.get_by_dn(dn)
+
+ # Optionally set country code
+ if country_code:
+ account.country_code = country_code
+
+ # Return account
+ return account
+
+ # Session stuff
def create_session(self, account, host):
- self._cleanup_expired_sessions()
+ session_id = util.random_string(64)
- res = self.db.get("INSERT INTO sessions(host, uid) VALUES(%s, %s) \
- RETURNING session_id, time_expires", host, account.uid)
+ res = self.db.get("INSERT INTO sessions(host, uid, session_id) VALUES(%s, %s, %s) \
+ RETURNING session_id, time_expires", host, account.uid, session_id)
# Session could not be created
if not res:
self.db.execute("DELETE FROM sessions \
WHERE session_id = %s AND host = %s", session_id, host)
- self._cleanup_expired_sessions()
def get_by_session(self, session_id, host):
logging.debug("Looking up session %s" % session_id)
WHERE session_id = %s AND host = %s", session_id, host)
return self.get_by_uid(res.uid)
-
+
+ def cleanup(self):
+ # Cleanup expired sessions
+ self.db.execute("DELETE FROM sessions WHERE time_expires <= NOW()")
+
+ # Cleanup expired account activations
+ self.db.execute("DELETE FROM account_activations WHERE expires_at <= NOW()")
+
+ # Discourse
+
+ def decode_discourse_payload(self, payload, signature):
+ # Check signature
+ calculated_signature = self.sign_discourse_payload(payload)
+
+ if not hmac.compare_digest(signature, calculated_signature):
+ raise ValueError("Invalid signature: %s" % signature)
+
+ # Decode the query string
+ qs = base64.b64decode(payload).decode()
+
+ # Parse the query string
+ data = {}
+ for key, val in urllib.parse.parse_qsl(qs):
+ data[key] = val
+
+ return data
+
+ def encode_discourse_payload(self, **args):
+ # Encode the arguments into an URL-formatted string
+ qs = urllib.parse.urlencode(args).encode()
+
+ # Encode into base64
+ return base64.b64encode(qs).decode()
+
+ def sign_discourse_payload(self, payload, secret=None):
+ if secret is None:
+ secret = self.settings.get("discourse_sso_secret")
+
+ # Calculate a HMAC using SHA256
+ h = hmac.new(secret.encode(),
+ msg=payload.encode(), digestmod="sha256")
+
+ return h.hexdigest()
+
class Account(Object):
def __init__(self, backend, dn, attrs=None):
Object.__init__(self, backend)
self.dn = dn
- self.__attrs = attrs or {}
+ self.attributes = attrs or {}
def __str__(self):
+ if self.nickname:
+ return self.nickname
+
return self.name
def __repr__(self):
def ldap(self):
return self.accounts.ldap
+ def _exists(self, key):
+ try:
+ self.attributes[key]
+ except KeyError:
+ return False
+
+ return True
+
+ def _get(self, key):
+ for value in self.attributes.get(key, []):
+ yield value
+
+ def _get_bytes(self, key, default=None):
+ for value in self._get(key):
+ return value
+
+ return default
+
+ def _get_strings(self, key):
+ for value in self._get(key):
+ yield value.decode()
+
+ def _get_string(self, key, default=None):
+ for value in self._get_strings(key):
+ return value
+
+ return default
+
+ def _get_phone_numbers(self, key):
+ for value in self._get_strings(key):
+ yield phonenumbers.parse(value, None)
+
+ def _get_timestamp(self, key):
+ value = self._get_string(key)
+
+ # Parse the timestamp value and returns a datetime object
+ if value:
+ return datetime.datetime.strptime(value, "%Y%m%d%H%M%SZ")
+
+ def _modify(self, modlist):
+ logging.debug("Modifying %s: %s" % (self.dn, modlist))
+
+ # Authenticate before performing any write operations
+ self.accounts._authenticate()
+
+ # Run modify operation
+ self.ldap.modify_s(self.dn, modlist)
+
+ # Delete cached attributes
+ self.memcache.delete("accounts:%s:attrs" % self.dn)
+
+ def _set(self, key, values):
+ current = self._get(key)
+
+ # Don't do anything if nothing has changed
+ if list(current) == values:
+ return
+
+ # Remove all old values and add all new ones
+ modlist = []
+
+ if self._exists(key):
+ modlist.append((ldap.MOD_DELETE, key, None))
+
+ # Add new values
+ if values:
+ modlist.append((ldap.MOD_ADD, key, values))
+
+ # Run modify operation
+ self._modify(modlist)
+
+ # Update cache
+ self.attributes.update({ key : values })
+
+ def _set_bytes(self, key, values):
+ return self._set(key, values)
+
+ def _set_strings(self, key, values):
+ return self._set(key, [e.encode() for e in values if e])
+
+ def _set_string(self, key, value):
+ return self._set_strings(key, [value,])
+
+ def _add(self, key, values):
+ modlist = [
+ (ldap.MOD_ADD, key, values),
+ ]
+
+ self._modify(modlist)
+
+ def _add_strings(self, key, values):
+ return self._add(key, [e.encode() for e in values])
+
+ def _add_string(self, key, value):
+ return self._add_strings(key, [value,])
+
+ def _delete(self, key, values):
+ modlist = [
+ (ldap.MOD_DELETE, key, values),
+ ]
+
+ self._modify(modlist)
+
+ def _delete_strings(self, key, values):
+ return self._delete(key, [e.encode() for e in values])
+
+ def _delete_string(self, key, value):
+ return self._delete_strings(key, [value,])
+
+ @lazy_property
+ def kerberos_attributes(self):
+ res = self.backend.accounts._query(
+ "(&(objectClass=krbPrincipal)(krbPrincipalName=%s@IPFIRE.ORG))" % self.uid,
+ attrlist=[
+ "krbLastSuccessfulAuth",
+ "krbLastPasswordChange",
+ "krbLastFailedAuth",
+ "krbLoginFailedCount",
+ ],
+ limit=1,
+ search_base="cn=krb5,%s" % self.backend.accounts.search_base)
+
+ for dn, attrs in res:
+ return { key : attrs[key][0] for key in attrs }
+
+ return {}
+
+ @staticmethod
+ def _parse_date(s):
+ return datetime.datetime.strptime(s.decode(), "%Y%m%d%H%M%SZ")
+
@property
- def attributes(self):
- return self.__attrs
+ def last_successful_authentication(self):
+ try:
+ s = self.kerberos_attributes["krbLastSuccessfulAuth"]
+ except KeyError:
+ return None
- def _get_first_attribute(self, attr, default=None):
- if attr not in self.attributes:
- return default
+ return self._parse_date(s)
- res = self.attributes.get(attr, [])
- if res:
- return res[0].decode()
+ @property
+ def last_failed_authentication(self):
+ try:
+ s = self.kerberos_attributes["krbLastFailedAuth"]
+ except KeyError:
+ return None
- def get(self, key):
+ return self._parse_date(s)
+
+ @property
+ def failed_login_count(self):
try:
- attribute = self.attributes[key]
+ count = self.kerberos_attributes["krbLoginFailedCount"].decode()
except KeyError:
- raise AttributeError(key)
+ return 0
- if len(attribute) == 1:
- return attribute[0]
+ try:
+ return int(count)
+ except ValueError:
+ return 0
- return attribute
+ def passwd(self, password):
+ """
+ Sets a new password
+ """
+ # The new password must have a score of 3 or better
+ quality = self.check_password_quality(password)
+ if quality["score"] < 3:
+ raise ValueError("Password too weak")
+
+ self.accounts._authenticate()
+ self.ldap.passwd_s(self.dn, None, password)
def check_password(self, password):
"""
Raises exceptions from the server on any other errors.
"""
+ if not password:
+ return
logging.debug("Checking credentials for %s" % self.dn)
+
+ # Create a new LDAP connection
+ ldap_uri = self.backend.settings.get("ldap_uri")
+ conn = ldap.initialize(ldap_uri)
+
try:
- self.ldap.simple_bind_s(self.dn, password.encode("utf-8"))
+ conn.simple_bind_s(self.dn, password.encode("utf-8"))
except ldap.INVALID_CREDENTIALS:
- logging.debug("Account credentials are invalid.")
+ logging.debug("Account credentials are invalid for %s" % self)
return False
- logging.debug("Successfully authenticated.")
+ logging.info("Successfully authenticated %s" % self)
+
return True
+ def check_password_quality(self, password):
+ """
+ Passwords are passed through zxcvbn to make sure
+ that they are strong enough.
+ """
+ return zxcvbn.zxcvbn(password, user_inputs=(
+ self.first_name, self.last_name,
+ ))
+
def is_admin(self):
- return "wheel" in self.groups
+ return self.is_member_of_group("sudo")
+
+ def is_staff(self):
+ return self.is_member_of_group("staff")
+
+ def is_moderator(self):
+ return self.is_member_of_group("moderators")
+
+ def has_shell(self):
+ return "posixAccount" in self.classes
+
+ def has_mail(self):
+ return "postfixMailUser" in self.classes
+
+ def has_sip(self):
+ return "sipUser" in self.classes or "sipRoutingObject" in self.classes
+
+ def can_be_managed_by(self, account):
+ """
+ Returns True if account is allowed to manage this account
+ """
+ # Admins can manage all accounts
+ if account.is_admin():
+ return True
- def is_talk_enabled(self):
- return "sipUser" in self.classes or "sipRoutingObject" in self.classes \
- or self.telephone_numbers or self.address
+ # Users can manage themselves
+ return self == account
@property
def classes(self):
- return (x.decode() for x in self.attributes.get("objectClass", []))
+ return self._get_strings("objectClass")
@property
def uid(self):
- return self._get_first_attribute("uid")
+ return self._get_string("uid")
@property
def name(self):
- return self._get_first_attribute("cn")
+ return self._get_string("cn")
- @property
- def first_name(self):
- return self._get_first_attribute("givenName")
+ # Nickname
- @property
+ def get_nickname(self):
+ return self._get_string("displayName")
+
+ def set_nickname(self, nickname):
+ self._set_string("displayName", nickname)
+
+ nickname = property(get_nickname, set_nickname)
+
+ # First Name
+
+ def get_first_name(self):
+ return self._get_string("givenName")
+
+ def set_first_name(self, first_name):
+ self._set_string("givenName", first_name)
+
+ # Update Common Name
+ self._set_string("cn", "%s %s" % (first_name, self.last_name))
+
+ first_name = property(get_first_name, set_first_name)
+
+ # Last Name
+
+ def get_last_name(self):
+ return self._get_string("sn")
+
+ def set_last_name(self, last_name):
+ self._set_string("sn", last_name)
+
+ # Update Common Name
+ self._set_string("cn", "%s %s" % (self.first_name, last_name))
+
+ last_name = property(get_last_name, set_last_name)
+
+ @lazy_property
def groups(self):
- if not hasattr(self, "_groups"):
- self._groups = []
+ return self.backend.groups._get_groups("(| \
+ (&(objectClass=groupOfNames)(member=%s)) \
+ (&(objectClass=posixGroup)(memberUid=%s)) \
+ )" % (self.dn, self.uid))
- res = self.accounts._search("(&(objectClass=posixGroup) \
- (memberUid=%s))" % self.uid, ["cn"])
+ def is_member_of_group(self, gid):
+ """
+ Returns True if this account is a member of this group
+ """
+ return gid in (g.gid for g in self.groups)
+
+ # Created/Modified at
+
+ @property
+ def created_at(self):
+ return self._get_timestamp("createTimestamp")
- for dn, attrs in res:
- cns = attrs.get("cn")
- if cns:
- self._groups.append(cns[0])
+ @property
+ def modified_at(self):
+ return self._get_timestamp("modifyTimestamp")
- return self._groups
+ # Address
@property
def address(self):
- address = self._get_first_attribute("homePostalAddress", "")
- address = address.replace(", ", "\n")
+ address = []
+
+ if self.street:
+ address += self.street.splitlines()
+
+ if self.postal_code and self.city:
+ if self.country_code in ("AT", "DE"):
+ address.append("%s %s" % (self.postal_code, self.city))
+ else:
+ address.append("%s, %s" % (self.city, self.postal_code))
+ else:
+ address.append(self.city or self.postal_code)
+
+ if self.country_name:
+ address.append(self.country_name)
return address
+ def get_street(self):
+ return self._get_string("street") or self._get_string("homePostalAddress")
+
+ def set_street(self, street):
+ self._set_string("street", street)
+
+ street = property(get_street, set_street)
+
+ def get_city(self):
+ return self._get_string("l") or ""
+
+ def set_city(self, city):
+ self._set_string("l", city)
+
+ city = property(get_city, set_city)
+
+ def get_postal_code(self):
+ return self._get_string("postalCode") or ""
+
+ def set_postal_code(self, postal_code):
+ self._set_string("postalCode", postal_code)
+
+ postal_code = property(get_postal_code, set_postal_code)
+
+ # XXX This should be c
+ def get_country_code(self):
+ return self._get_string("st")
+
+ def set_country_code(self, country_code):
+ self._set_string("st", country_code)
+
+ country_code = property(get_country_code, set_country_code)
+
+ @property
+ def country_name(self):
+ if self.country_code:
+ return countries.get_name(self.country_code)
+
@property
def email(self):
- name = self.name.lower()
- name = name.replace(" ", ".")
- name = name.replace("Ä", "Ae")
- name = name.replace("Ö", "Oe")
- name = name.replace("Ü", "Ue")
- name = name.replace("ä", "ae")
- name = name.replace("ö", "oe")
- name = name.replace("ü", "ue")
-
- for mail in self.attributes.get("mail", []):
- if mail.decode().startswith("%s@ipfire.org" % name):
- return mail
-
- # If everything else fails, we will go with the UID
- return "%s@ipfire.org" % self.uid
+ return self._get_string("mail")
+
+ # Mail Routing Address
+
+ def get_mail_routing_address(self):
+ return self._get_string("mailRoutingAddress", None)
+
+ def set_mail_routing_address(self, address):
+ self._set_string("mailRoutingAddress", address or None)
+
+ mail_routing_address = property(get_mail_routing_address, set_mail_routing_address)
@property
def sip_id(self):
if "sipUser" in self.classes:
- return self._get_first_attribute("sipAuthenticationUser")
+ return self._get_string("sipAuthenticationUser")
if "sipRoutingObject" in self.classes:
- return self._get_first_attribute("sipLocalAddress")
+ return self._get_string("sipLocalAddress")
@property
def sip_password(self):
- return self._get_first_attribute("sipPassword")
+ return self._get_string("sipPassword")
+
+ @staticmethod
+ def _generate_sip_password():
+ return util.random_string(8)
@property
def sip_url(self):
return "%s@ipfire.org" % self.sip_id
def uses_sip_forwarding(self):
- if self.sip_routing_url:
+ if self.sip_routing_address:
return True
return False
- @property
- def sip_routing_url(self):
+ # SIP Routing
+
+ def get_sip_routing_address(self):
if "sipRoutingObject" in self.classes:
- return self._get_first_attribute("sipRoutingAddress")
+ return self._get_string("sipRoutingAddress")
- def sip_is_online(self):
- assert self.sip_id
+ def set_sip_routing_address(self, address):
+ if not address:
+ address = None
- if not hasattr(self, "_is_online"):
- self._is_online = self.backend.talk.user_is_online(self.sip_id)
+ # Don't do anything if nothing has changed
+ if self.get_sip_routing_address() == address:
+ return
- return self._is_online
+ if address:
+ # This is no longer a SIP user any more
+ try:
+ self._modify([
+ (ldap.MOD_DELETE, "objectClass", b"sipUser"),
+ (ldap.MOD_DELETE, "sipAuthenticationUser", None),
+ (ldap.MOD_DELETE, "sipPassword", None),
+ ])
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
+ # Set new routing object
+ try:
+ self._modify([
+ (ldap.MOD_ADD, "objectClass", b"sipRoutingObject"),
+ (ldap.MOD_ADD, "sipLocalAddress", self.sip_id.encode()),
+ (ldap.MOD_ADD, "sipRoutingAddress", address.encode()),
+ ])
+
+ # If this is a change, we cannot add this again
+ except ldap.TYPE_OR_VALUE_EXISTS:
+ self._set_string("sipRoutingAddress", address)
+ else:
+ try:
+ self._modify([
+ (ldap.MOD_DELETE, "objectClass", b"sipRoutingObject"),
+ (ldap.MOD_DELETE, "sipLocalAddress", None),
+ (ldap.MOD_DELETE, "sipRoutingAddress", None),
+ ])
+ except ldap.NO_SUCH_ATTRIBUTE:
+ pass
+
+ self._modify([
+ (ldap.MOD_ADD, "objectClass", b"sipUser"),
+ (ldap.MOD_ADD, "sipAuthenticationUser", self.sip_id.encode()),
+ (ldap.MOD_ADD, "sipPassword", self._generate_sip_password().encode()),
+ ])
+
+ # XXX Cache is invalid here
+
+ sip_routing_address = property(get_sip_routing_address, set_sip_routing_address)
@lazy_property
def sip_registrations(self):
return sip_registrations
- @property
- def telephone_numbers(self):
- return self._telephone_numbers + self.mobile_telephone_numbers \
- + self.home_telephone_numbers
+ @lazy_property
+ def sip_channels(self):
+ return self.backend.talk.freeswitch.get_sip_channels(self)
- @property
- def _telephone_numbers(self):
- return self.attributes.get("telephoneNumber") or []
+ def get_cdr(self, date=None, limit=None):
+ return self.backend.talk.freeswitch.get_cdr_by_account(self, date=date, limit=limit)
- @property
- def home_telephone_numbers(self):
- return self.attributes.get("homePhone") or []
+ # Phone Numbers
+
+ @lazy_property
+ def phone_number(self):
+ """
+ Returns the IPFire phone number
+ """
+ if self.sip_id:
+ return phonenumbers.parse("+4923636035%s" % self.sip_id)
+
+ @lazy_property
+ def fax_number(self):
+ if self.sip_id:
+ return phonenumbers.parse("+49236360359%s" % self.sip_id)
+
+ def get_phone_numbers(self):
+ ret = []
+
+ for field in ("telephoneNumber", "homePhone", "mobile"):
+ for number in self._get_phone_numbers(field):
+ ret.append(number)
+
+ return ret
+
+ def set_phone_numbers(self, phone_numbers):
+ # Sort phone numbers by landline and mobile
+ _landline_numbers = []
+ _mobile_numbers = []
+
+ for number in phone_numbers:
+ try:
+ number = phonenumbers.parse(number, None)
+ except phonenumbers.phonenumberutil.NumberParseException:
+ continue
+
+ # Convert to string (in E.164 format)
+ s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
+
+ # Separate mobile numbers
+ if phonenumbers.number_type(number) == phonenumbers.PhoneNumberType.MOBILE:
+ _mobile_numbers.append(s)
+ else:
+ _landline_numbers.append(s)
+
+ # Save
+ self._set_strings("telephoneNumber", _landline_numbers)
+ self._set_strings("mobile", _mobile_numbers)
+
+ phone_numbers = property(get_phone_numbers, set_phone_numbers)
@property
- def mobile_telephone_numbers(self):
- return self.attributes.get("mobile") or []
+ def _all_telephone_numbers(self):
+ ret = [ self.sip_id, ]
- def avatar_url(self, size=None):
- if self.backend.debug:
- hostname = "accounts.dev.ipfire.org"
- else:
- hostname = "accounts.ipfire.org"
+ if self.phone_number:
+ s = phonenumbers.format_number(self.phone_number, phonenumbers.PhoneNumberFormat.E164)
+ ret.append(s)
+
+ for number in self.phone_numbers:
+ s = phonenumbers.format_number(number, phonenumbers.PhoneNumberFormat.E164)
+ ret.append(s)
- url = "https://%s/avatar/%s.jpg" % (hostname, self.uid)
+ return ret
+
+ def has_avatar(self):
+ has_avatar = self.memcache.get("accounts:%s:has-avatar" % self.uid)
+ if has_avatar is None:
+ has_avatar = True if self.get_avatar() else False
+
+ # Cache avatar status for up to 24 hours
+ self.memcache.set("accounts:%s:has-avatar" % self.uid, has_avatar, 3600 * 24)
+
+ return has_avatar
+
+ def avatar_url(self, size=None):
+ url = "https://people.ipfire.org/users/%s.jpg" % self.uid
if size:
url += "?size=%s" % size
return url
def get_avatar(self, size=None):
- avatar = self._get_first_attribute("jpegPhoto")
- if not avatar:
+ photo = self._get_bytes("jpegPhoto")
+
+ # Exit if no avatar is available
+ if not photo:
return
- if not size:
+ # Return the raw image if no size was requested
+ if size is None:
+ return photo
+
+ # Try to retrieve something from the cache
+ avatar = self.memcache.get("accounts:%s:avatar:%s" % (self.dn, size))
+ if avatar:
return avatar
- return self._resize_avatar(avatar, size)
+ # Generate a new thumbnail
+ avatar = util.generate_thumbnail(photo, size, square=True)
+
+ # Save to cache for 15m
+ self.memcache.set("accounts:%s:avatar:%s" % (self.dn, size), avatar, 900)
+
+ return avatar
+
+ def upload_avatar(self, avatar):
+ self._set("jpegPhoto", avatar)
- def _resize_avatar(self, image, size):
- image = io.StringIO(image)
- image = PIL.Image.open(image)
+ # Delete cached avatar status
+ self.memcache.delete("accounts:%s:has-avatar" % self.uid)
- # Resize the image to the desired resolution
- image.thumbnail((size, size), PIL.Image.ANTIALIAS)
- f = io.StringIO()
+class StopForumSpam(Object):
+ def init(self, uid, email, address):
+ self.uid, self.email, self.address = uid, email, address
+
+ @tornado.gen.coroutine
+ def send_request(self, **kwargs):
+ arguments = {
+ "json" : "1",
+ }
+ arguments.update(kwargs)
+
+ # Create request
+ request = tornado.httpclient.HTTPRequest(
+ "https://api.stopforumspam.org/api", method="POST")
+ request.body = urllib.parse.urlencode(arguments)
+
+ # Send the request
+ response = yield self.backend.http_client.fetch(request)
+
+ # Decode the JSON response
+ return json.loads(response.body.decode())
+
+ @tornado.gen.coroutine
+ def check_address(self):
+ response = yield self.send_request(ip=self.address)
+
+ try:
+ confidence = response["ip"]["confidence"]
+ except KeyError:
+ confidence = 100
+
+ logging.debug("Confidence for %s: %s" % (self.address, confidence))
+
+ return confidence
+
+ @tornado.gen.coroutine
+ def check_username(self):
+ response = yield self.send_request(username=self.uid)
- # If writing out the image does not work with optimization,
- # we try to write it out without any optimization.
try:
- image.save(f, "JPEG", optimize=True)
- except:
- image.save(f, "JPEG")
+ confidence = response["username"]["confidence"]
+ except KeyError:
+ confidence = 100
+
+ logging.debug("Confidence for %s: %s" % (self.uid, confidence))
+
+ return confidence
+
+ @tornado.gen.coroutine
+ def check_email(self):
+ response = yield self.send_request(email=self.email)
+
+ try:
+ confidence = response["email"]["confidence"]
+ except KeyError:
+ confidence = 100
+
+ logging.debug("Confidence for %s: %s" % (self.email, confidence))
+
+ return confidence
+
+ @tornado.gen.coroutine
+ def check(self, threshold=95):
+ """
+ This function tries to detect if we have a spammer.
+
+ To honour the privacy of our users, we only send the IP
+ address and username and if those are on the database, we
+ will send the email address as well.
+ """
+ confidences = yield [self.check_address(), self.check_username()]
+
+ if any((c < threshold for c in confidences)):
+ confidences += yield [self.check_email()]
+
+ # Build a score based on the lowest confidence
+ return 100 - min(confidences)
+
+
+class Groups(Object):
+ hidden_groups = (
+ "cn=LDAP Read Only,ou=Group,dc=ipfire,dc=org",
+ "cn=LDAP Read Write,ou=Group,dc=ipfire,dc=org",
+ "cn=sudo,ou=Group,dc=ipfire,dc=org",
+
+ # Everyone is a member of people
+ "cn=people,ou=Group,dc=ipfire,dc=org",
+ )
+
+ @property
+ def search_base(self):
+ return "ou=Group,%s" % self.backend.accounts.search_base
+
+ def _query(self, *args, **kwargs):
+ kwargs.update({
+ "search_base" : self.backend.groups.search_base,
+ })
- return f.getvalue()
+ return self.backend.accounts._query(*args, **kwargs)
+
+ def __iter__(self):
+ groups = self.get_all()
+
+ return iter(groups)
+
+ def _get_groups(self, query, **kwargs):
+ res = self._query(query, **kwargs)
+
+ groups = []
+ for dn, attrs in res:
+ # Skip any hidden groups
+ if dn in self.hidden_groups:
+ continue
+
+ g = Group(self.backend, dn, attrs)
+ groups.append(g)
+
+ return sorted(groups)
+
+ def _get_group(self, query, **kwargs):
+ kwargs.update({
+ "limit" : 1,
+ })
+
+ groups = self._get_groups(query, **kwargs)
+ if groups:
+ return groups[0]
+
+ def get_all(self):
+ return self._get_groups(
+ "(|(objectClass=posixGroup)(objectClass=groupOfNames))",
+ )
+ def get_by_gid(self, gid):
+ return self._get_group(
+ "(&(|(objectClass=posixGroup)(objectClass=groupOfNames))(cn=%s))" % gid,
+ )
+
+
+class Group(Object):
+ def init(self, dn, attrs=None):
+ self.dn = dn
+
+ self.attributes = attrs or {}
+
+ def __repr__(self):
+ if self.description:
+ return "<%s %s (%s)>" % (
+ self.__class__.__name__,
+ self.gid,
+ self.description,
+ )
+
+ return "<%s %s>" % (self.__class__.__name__, self.gid)
+
+ def __str__(self):
+ return self.description or self.gid
+
+ def __eq__(self, other):
+ if isinstance(other, self.__class__):
+ return self.gid == other.gid
+
+ def __lt__(self, other):
+ if isinstance(other, self.__class__):
+ return (self.description or self.gid) < (other.description or other.gid)
+
+ def __bool__(self):
+ return True
+
+ def __len__(self):
+ """
+ Returns the number of members in this group
+ """
+ l = 0
+
+ for attr in ("member", "memberUid"):
+ a = self.attributes.get(attr, None)
+ if a:
+ l += len(a)
+
+ return l
+
+ def __iter__(self):
+ return iter(self.members)
+
+ @property
+ def gid(self):
+ try:
+ gid = self.attributes["cn"][0]
+ except KeyError:
+ return None
+
+ return gid.decode()
+
+ @property
+ def description(self):
+ try:
+ description = self.attributes["description"][0]
+ except KeyError:
+ return None
+
+ return description.decode()
+
+ @property
+ def email(self):
+ try:
+ email = self.attributes["mail"][0]
+ except KeyError:
+ return None
+
+ return email.decode()
+
+ @lazy_property
+ def members(self):
+ members = []
+
+ # Get all members by DN
+ for dn in self.attributes.get("member", []):
+ member = self.backend.accounts.get_by_dn(dn.decode())
+ if member:
+ members.append(member)
+
+ # Get all meembers by UID
+ for uid in self.attributes.get("memberUid", []):
+ member = self.backend.accounts.get_by_uid(uid.decode())
+ if member:
+ members.append(member)
+
+ return sorted(members)
if __name__ == "__main__":
a = Accounts()