From 04078ce1e099c2dc4755a41ee80ddc001b3ddfcd Mon Sep 17 00:00:00 2001 From: Sistason <c3infra@sistason.de> Date: Wed, 10 Mar 2021 00:53:46 +0100 Subject: [PATCH] If LDAP_SERVICE_BIND_DN is empty, use the current users credentials for all LDAP actions. The user_password is stored in the cookie for that reason. Also, ensure teardown of all opened LDAP connection afterwards --- uffd/__init__.py | 7 ++++++- uffd/ldap.py | 39 +++++++++++++++++++++++++++++++++------ uffd/selfservice/views.py | 7 ++++++- uffd/session/views.py | 33 ++++++++++++++++++++++++--------- 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/uffd/__init__.py b/uffd/__init__.py index e124a3ce..cc7926a7 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 2d7f6f93..c9aa3838 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 40dc49e0..93b5838b 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 0331f2f1..b401563e 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(): -- GitLab