diff --git a/uffd/__init__.py b/uffd/__init__.py index e124a3ce1209e0fd10d280aa32fa065a1d2f67e3..cc7926a7b8b43e31e9e48071318088973c984b20 100644 --- a/uffd/__init__.py +++ b/uffd/__init__.py @@ -2,7 +2,7 @@ import os import secrets import sys -from flask import Flask, redirect, url_for +from flask import Flask, redirect, url_for, request from werkzeug.routing import IntegerConverter sys.path.append('deps/ldapalchemy') @@ -53,6 +53,11 @@ def create_app(test_config=None): # pylint: disable=too-many-locals def index(): #pylint: disable=unused-variable return redirect(url_for('selfservice.index')) + @app.teardown_request + def close_connection(e): + if hasattr(request, "ldap_connection"): + request.ldap_connection.unbind() + return app def init_db(app): diff --git a/uffd/ldap.py b/uffd/ldap.py index 2d7f6f93d8d06f219edda955668d69021fd21aa2..c9aa38388e95169138c34169bec566c57fd38d59 100644 --- a/uffd/ldap.py +++ b/uffd/ldap.py @@ -1,10 +1,12 @@ -from flask import current_app, request, abort +from flask import current_app, request, abort, session import ldap3 +from ldap3.core.exceptions import LDAPBindError -from ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import +from ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import from ldapalchemy.model import Query + class FlaskQuery(Query): def get_or_404(self, dn): res = self.get(dn) @@ -18,13 +20,28 @@ class FlaskQuery(Query): abort(404) return res + +def connect_and_bind_to_ldap(server, bind_dn, bind_pw): + # Using auto_bind cannot close the connection, so define the connection with extra steps + _connection = ldap3.Connection(server, bind_dn, bind_pw) + if _connection.closed: + _connection.open(read_server_info=False) + if current_app.config["LDAP_SERVICE_USE_STARTTLS"]: + _connection.start_tls(read_server_info=False) + if not _connection.bind(read_server_info=True): + _connection.unbind() + return None + return _connection + + class FlaskLDAPMapper(LDAPMapper): def __init__(self): super().__init__() + class Model(self.Model): query_class = FlaskQuery - self.Model = Model # pylint: disable=invalid-name + self.Model = Model # pylint: disable=invalid-name @property def session(self): @@ -48,9 +65,19 @@ class FlaskLDAPMapper(LDAPMapper): current_app.ldap_mock.bind() return current_app.ldap_mock server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"], get_info=ldap3.ALL) - auto_bind = ldap3.AUTO_BIND_TLS_BEFORE_BIND if current_app.config["LDAP_SERVICE_USE_STARTTLS"] else True - request.ldap_connection = ldap3.Connection(server, current_app.config["LDAP_SERVICE_BIND_DN"], - current_app.config["LDAP_SERVICE_BIND_PASSWORD"], auto_bind=auto_bind) + # If the configured LDAP service bind_dn is empty, connect to LDAP as a user + if current_app.config['LDAP_SERVICE_BIND_DN']: + bind_dn = current_app.config["LDAP_SERVICE_BIND_DN"] + bind_pw = current_app.config["LDAP_SERVICE_BIND_PASSWORD"] + else: + bind_dn = session['user_dn'] + bind_pw = session['user_pw'] + + request.ldap_connection = connect_and_bind_to_ldap(server, bind_dn, bind_pw) + if not request.ldap_connection: + raise LDAPBindError + return request.ldap_connection + ldap = FlaskLDAPMapper() diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py index 40dc49e010832ed5c5d1d1add343842ad6f2eb61..93b5838b6c80cb5c14a15aee4072c9cc61c973f7 100644 --- a/uffd/selfservice/views.py +++ b/uffd/selfservice/views.py @@ -4,7 +4,7 @@ import smtplib from email.message import EmailMessage import email.utils -from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app +from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session from uffd.navbar import register_navbar from uffd.csrf import csrf_protect @@ -29,6 +29,7 @@ def index(): @csrf_protect(blueprint=bp) @login_required() def update(): + password_changed = False user = get_current_user() if request.values['displayname'] != user.displayname: if user.set_displayname(request.values['displayname']): @@ -41,12 +42,16 @@ def update(): else: if user.set_password(request.values['password1']): flash('Password changed.') + password_changed = True else: flash('Password could not be set.') if request.values['mail'] != user.mail: send_mail_verification(user.loginname, request.values['mail']) flash('We sent you an email, please verify your mail address.') ldap.session.commit() + # When using a user_connection, update the connection on password-change + if password_changed and not current_app.config['LDAP_SERVICE_BIND_DN']: + session['user_pw'] = request.values['password1'] return redirect(url_for('selfservice.index')) @bp.route("/passwordreset", methods=(['GET', 'POST'])) diff --git a/uffd/session/views.py b/uffd/session/views.py index 0331f2f103f2f2bc9c0459164fa911acc4209755..b401563e0099803c06dcfd0bd6beb1625e05bcd0 100644 --- a/uffd/session/views.py +++ b/uffd/session/views.py @@ -9,7 +9,7 @@ from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError from ldapalchemy.core import encode_filter from uffd.user.models import User -from uffd.ldap import ldap +from uffd.ldap import ldap, connect_and_bind_to_ldap from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/') @@ -31,12 +31,23 @@ def login_get_user(loginname, password): except (LDAPBindError, LDAPPasswordIsMandatoryError): return None else: - server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"], get_info=ldap3.ALL) - auto_bind = ldap3.AUTO_BIND_TLS_BEFORE_BIND if current_app.config["LDAP_SERVICE_USE_STARTTLS"] else True - try: - conn = ldap3.Connection(server, dn, password, auto_bind=auto_bind) - except (LDAPBindError, LDAPPasswordIsMandatoryError): - return None + # When using a LDAP service connection, try bind with separate user connection + if current_app.config['LDAP_SERVICE_BIND_DN']: + server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"], get_info=ldap3.ALL) + try: + conn = connect_and_bind_to_ldap(server, dn, password) + except (LDAPBindError, LDAPPasswordIsMandatoryError): + return None + else: + session.clear() + session['user_dn'] = dn + session['user_pw'] = password + try: + conn = ldap.get_connection() + except (LDAPBindError, LDAPPasswordIsMandatoryError): + session.clear() + return None + conn.search(conn.user, encode_filter(current_app.config["LDAP_USER_SEARCH_FILTER"])) if len(conn.entries) != 1: return None @@ -50,9 +61,11 @@ def logout(): session.clear() return resp -def set_session(user, skip_mfa=False): +def set_session(user, skip_mfa=False, password=''): session.clear() session['user_dn'] = user.dn + if password: + session['user_pw'] = password session['logintime'] = datetime.datetime.now().timestamp() session['_csrf_token'] = secrets.token_hex(128) if skip_mfa: @@ -82,7 +95,9 @@ def login(): if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']): flash('You do not have access to this service') return render_template('login.html', ref=request.values.get('ref')) - set_session(user) + # If the configured LDAP bind_dn is empty, connect to LDAP as a user + # Therefore, we save the password for future binds in the session + set_session(user, password=password if not current_app.config['LDAP_SERVICE_BIND_DN'] else '') return redirect(url_for('mfa.auth', ref=request.values.get('ref', url_for('index')))) def get_current_user():