Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • ldap_user_conn_test
  • master
2 results

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • strifel/uffd
  • thies/uffd-2
6 results
Select Git revision
  • Dockerfile
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
30 results
Show changes

Commits on Source 13

...@@ -31,6 +31,15 @@ Use uwsgi. ...@@ -31,6 +31,15 @@ Use uwsgi.
tabs. tabs.
## Bind with service account or as user?
Uffd can use a dedicated service account for LDAP operations by setting `LDAP_SERVICE_BIND_DN`.
Or it uses the credentials of the currently logged in user, by not setting `LDAP_SERVICE_BIND_DN`.
If you choose to run with user credentials, some features are not available, like password resets
or self signup, since in both cases, no user credentials can exist.
## OAuth2 Single-Sign-On Provider ## OAuth2 Single-Sign-On Provider
Other services can use uffd as an OAuth2.0-based authentication provider. Other services can use uffd as an OAuth2.0-based authentication provider.
......
...@@ -135,3 +135,6 @@ class TestSession(UffdTestCase): ...@@ -135,3 +135,6 @@ class TestSession(UffdTestCase):
class TestSessionOL(TestSession): class TestSessionOL(TestSession):
use_openldap = True use_openldap = True
class TestSessionOLUser(TestSessionOL):
use_userconnection = True
...@@ -25,6 +25,7 @@ def db_flush(): ...@@ -25,6 +25,7 @@ def db_flush():
class UffdTestCase(unittest.TestCase): class UffdTestCase(unittest.TestCase):
use_openldap = False use_openldap = False
use_userconnection = False
def setUp(self): def setUp(self):
self.dir = tempfile.mkdtemp() self.dir = tempfile.mkdtemp()
...@@ -43,6 +44,9 @@ class UffdTestCase(unittest.TestCase): ...@@ -43,6 +44,9 @@ class UffdTestCase(unittest.TestCase):
self.skipTest('OPENLDAP_TESTING not set') self.skipTest('OPENLDAP_TESTING not set')
config['LDAP_SERVICE_MOCK'] = False config['LDAP_SERVICE_MOCK'] = False
config['LDAP_SERVICE_URL'] = 'ldap://localhost' config['LDAP_SERVICE_URL'] = 'ldap://localhost'
if self.use_userconnection:
config['LDAP_SERVICE_BIND_DN'] = None
else:
config['LDAP_SERVICE_BIND_DN'] = 'cn=uffd,ou=system,dc=example,dc=com' config['LDAP_SERVICE_BIND_DN'] = 'cn=uffd,ou=system,dc=example,dc=com'
config['LDAP_SERVICE_BIND_PASSWORD'] = 'uffd-ldap-password' config['LDAP_SERVICE_BIND_PASSWORD'] = 'uffd-ldap-password'
os.system("ldapdelete -c -D 'cn=uffd,ou=system,dc=example,dc=com' -w 'uffd-ldap-password' -H 'ldap://localhost' -f ldap_server_entries_cleanup.ldif > /dev/null 2>&1") os.system("ldapdelete -c -D 'cn=uffd,ou=system,dc=example,dc=com' -w 'uffd-ldap-password' -H 'ldap://localhost' -f ldap_server_entries_cleanup.ldif > /dev/null 2>&1")
......
...@@ -2,7 +2,7 @@ import os ...@@ -2,7 +2,7 @@ import os
import secrets import secrets
import sys import sys
from flask import Flask, redirect, url_for from flask import Flask, redirect, url_for, request
from werkzeug.routing import IntegerConverter from werkzeug.routing import IntegerConverter
sys.path.append('deps/ldapalchemy') sys.path.append('deps/ldapalchemy')
...@@ -42,17 +42,33 @@ def create_app(test_config=None): # pylint: disable=too-many-locals ...@@ -42,17 +42,33 @@ def create_app(test_config=None): # pylint: disable=too-many-locals
pass pass
db.init_app(app) db.init_app(app)
# pylint: disable=C0415 # pylint: disable=C0415
from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, services, signup, invite from uffd import user, selfservice, role, mail, session, csrf, mfa, oauth2, services
# pylint: enable=C0415 # pylint: enable=C0415
for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + mfa.bp + oauth2.bp + services.bp + signup.bp + invite.bp: for i in user.bp + selfservice.bp + role.bp + mail.bp + session.bp + csrf.bp + mfa.bp + oauth2.bp + services.bp:
app.register_blueprint(i)
if app.config['LDAP_SERVICE_BIND_DN'] or app.config.get('LDAP_SERVICE_MOCK', False):
# pylint: disable=C0415
from uffd import signup, invite
# pylint: enable=C0415
for i in signup.bp + invite.bp:
app.register_blueprint(i) app.register_blueprint(i)
else:
app.config['ENABLE_PASSWORDRESET'] = False
@app.route("/") @app.route("/")
def index(): #pylint: disable=unused-variable def index(): #pylint: disable=unused-variable
return redirect(url_for('selfservice.index')) return redirect(url_for('selfservice.index'))
@app.teardown_request
def close_connection(exception): #pylint: disable=unused-variable,unused-argument
if hasattr(request, "ldap_connection"):
request.ldap_connection.unbind()
return app return app
def init_db(app): def init_db(app):
......
...@@ -37,6 +37,8 @@ LDAP_MAIL_UID_ATTRIBUTE="uid" ...@@ -37,6 +37,8 @@ LDAP_MAIL_UID_ATTRIBUTE="uid"
LDAP_MAIL_RECEIVERS_ATTRIBUTE="mailacceptinggeneralid" LDAP_MAIL_RECEIVERS_ATTRIBUTE="mailacceptinggeneralid"
LDAP_MAIL_DESTINATIONS_ATTRIBUTE="maildrop" LDAP_MAIL_DESTINATIONS_ATTRIBUTE="maildrop"
# If you do not set the service bind_dn, connections use the user credentials.
# When using a user connection, some features are not available, since they require a service connection
LDAP_SERVICE_BIND_DN="" LDAP_SERVICE_BIND_DN=""
LDAP_SERVICE_BIND_PASSWORD="" LDAP_SERVICE_BIND_PASSWORD=""
LDAP_SERVICE_URL="ldapi:///" LDAP_SERVICE_URL="ldapi:///"
...@@ -60,6 +62,8 @@ MAIL_FROM_ADDRESS='foo@bar.com' ...@@ -60,6 +62,8 @@ MAIL_FROM_ADDRESS='foo@bar.com'
# Do not enable this on a public service! There is no spam protection implemented at the moment. # Do not enable this on a public service! There is no spam protection implemented at the moment.
SELF_SIGNUP=False SELF_SIGNUP=False
# PASSWORDRESET is not available when not using a service connection
ENABLE_PASSWORDRESET=True
#MFA_ICON_URL = 'https://example.com/logo.png' #MFA_ICON_URL = 'https://example.com/logo.png'
#MFA_RP_ID = 'example.com' # If unset, hostname from current request is used #MFA_RP_ID = 'example.com' # If unset, hostname from current request is used
......
from flask import current_app, request, abort from flask import current_app, request, abort, session
import ldap3 import ldap3
from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError, LDAPInvalidDnError
from ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import from ldapalchemy import LDAPMapper, LDAPCommitError # pylint: disable=unused-import
from ldapalchemy.model import Query from ldapalchemy.model import Query
from ldapalchemy.core import encode_filter
class FlaskQuery(Query): class FlaskQuery(Query):
def get_or_404(self, dn): def get_or_404(self, dn):
...@@ -18,9 +21,49 @@ class FlaskQuery(Query): ...@@ -18,9 +21,49 @@ class FlaskQuery(Query):
abort(404) abort(404)
return res return res
def test_user_bind(bind_dn, bind_pw):
try:
if current_app.config.get('LDAP_SERVICE_MOCK', False):
# Since we reuse the same conn for all calls to `user_conn()` we
# simulate the password check by rebinding. Note that ldap3's mocking
# implementation just compares the string in the objects's userPassword
# field with the password, no support for hashing or OpenLDAP-style
# password-prefixes ("{PLAIN}..." or "{ssha512}...").
conn = ldap.get_connection()
if not conn.rebind(bind_dn, bind_pw):
return False
else:
server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"])
conn = connect_and_bind_to_ldap(server, bind_dn, bind_pw)
if not conn:
return False
except (LDAPBindError, LDAPPasswordIsMandatoryError):
return False
conn.search(conn.user, encode_filter(current_app.config["LDAP_USER_SEARCH_FILTER"]))
lazy_entries = conn.entries
conn.unbind()
return len(lazy_entries) == 1
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()
raise LDAPBindError
return _connection
class FlaskLDAPMapper(LDAPMapper): class FlaskLDAPMapper(LDAPMapper):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
class Model(self.Model): class Model(self.Model):
query_class = FlaskQuery query_class = FlaskQuery
...@@ -48,9 +91,20 @@ class FlaskLDAPMapper(LDAPMapper): ...@@ -48,9 +91,20 @@ class FlaskLDAPMapper(LDAPMapper):
current_app.ldap_mock.bind() current_app.ldap_mock.bind()
return current_app.ldap_mock return current_app.ldap_mock
server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"], get_info=ldap3.ALL) 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"], # If the configured LDAP service bind_dn is empty, connect to LDAP as a user
current_app.config["LDAP_SERVICE_BIND_PASSWORD"], auto_bind=auto_bind) 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']
try:
request.ldap_connection = connect_and_bind_to_ldap(server, bind_dn, bind_pw)
except (LDAPBindError, LDAPPasswordIsMandatoryError):
return None
return request.ldap_connection return request.ldap_connection
ldap = FlaskLDAPMapper() ldap = FlaskLDAPMapper()
...@@ -4,7 +4,7 @@ import smtplib ...@@ -4,7 +4,7 @@ import smtplib
from email.message import EmailMessage from email.message import EmailMessage
import email.utils 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.navbar import register_navbar
from uffd.csrf import csrf_protect from uffd.csrf import csrf_protect
...@@ -29,6 +29,7 @@ def index(): ...@@ -29,6 +29,7 @@ def index():
@csrf_protect(blueprint=bp) @csrf_protect(blueprint=bp)
@login_required() @login_required()
def update(): def update():
password_changed = False
user = get_current_user() user = get_current_user()
if request.values['displayname'] != user.displayname: if request.values['displayname'] != user.displayname:
if user.set_displayname(request.values['displayname']): if user.set_displayname(request.values['displayname']):
...@@ -41,12 +42,16 @@ def update(): ...@@ -41,12 +42,16 @@ def update():
else: else:
if user.set_password(request.values['password1']): if user.set_password(request.values['password1']):
flash('Password changed.') flash('Password changed.')
password_changed = True
else: else:
flash('Password could not be set.') flash('Password could not be set.')
if request.values['mail'] != user.mail: if request.values['mail'] != user.mail:
send_mail_verification(user.loginname, request.values['mail']) send_mail_verification(user.loginname, request.values['mail'])
flash('We sent you an email, please verify your mail address.') flash('We sent you an email, please verify your mail address.')
ldap.session.commit() 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')) return redirect(url_for('selfservice.index'))
@bp.route("/passwordreset", methods=(['GET', 'POST'])) @bp.route("/passwordreset", methods=(['GET', 'POST']))
......
...@@ -25,7 +25,9 @@ ...@@ -25,7 +25,9 @@
{% if config['SELF_SIGNUP'] %} {% if config['SELF_SIGNUP'] %}
<a href="{{ url_for("signup.signup_start") }}" class="float-left">Register</a> <a href="{{ url_for("signup.signup_start") }}" class="float-left">Register</a>
{% endif %} {% endif %}
{% if config['ENABLE_PASSWORDRESET'] %}
<a href="{{ url_for("selfservice.forgot_password") }}" class="float-right">Forgot Password?</a> <a href="{{ url_for("selfservice.forgot_password") }}" class="float-right">Forgot Password?</a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -4,12 +4,8 @@ import functools ...@@ -4,12 +4,8 @@ import functools
from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session, abort from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session, abort
import ldap3
from ldap3.core.exceptions import LDAPBindError, LDAPPasswordIsMandatoryError
from ldapalchemy.core import encode_filter
from uffd.user.models import User from uffd.user.models import User
from uffd.ldap import ldap from uffd.ldap import ldap, test_user_bind, LDAPInvalidDnError
from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay from uffd.ratelimit import Ratelimit, host_ratelimit, format_delay
bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/') bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/')
...@@ -18,29 +14,26 @@ login_ratelimit = Ratelimit('login', 1*60, 3) ...@@ -18,29 +14,26 @@ login_ratelimit = Ratelimit('login', 1*60, 3)
def login_get_user(loginname, password): def login_get_user(loginname, password):
dn = User(loginname=loginname).dn dn = User(loginname=loginname).dn
if current_app.config.get('LDAP_SERVICE_MOCK', False):
conn = ldap.get_connection() # If we use a service connection, test user bind seperately
# Since we reuse the same conn for all calls to `user_conn()` we if current_app.config['LDAP_SERVICE_BIND_DN'] or current_app.config.get('LDAP_SERVICE_MOCK', False):
# simulate the password check by rebinding. Note that ldap3's mocking if not test_user_bind(dn, password):
# implementation just compares the string in the objects's userPassword
# field with the password, no support for hashing or OpenLDAP-style
# password-prefixes ("{PLAIN}..." or "{ssha512}...").
try:
if not conn.rebind(dn, password):
return None
except (LDAPBindError, LDAPPasswordIsMandatoryError):
return None return None
# If we use a user connection, just create the connection normally
else: else:
server = ldap3.Server(current_app.config["LDAP_SERVICE_URL"], get_info=ldap3.ALL) # ldap.get_connection gets the credentials from the session, so set it here initially
auto_bind = ldap3.AUTO_BIND_TLS_BEFORE_BIND if current_app.config["LDAP_SERVICE_USE_STARTTLS"] else True session['user_dn'] = dn
try: session['user_pw'] = password
conn = ldap3.Connection(server, dn, password, auto_bind=auto_bind) if not ldap.get_connection():
except (LDAPBindError, LDAPPasswordIsMandatoryError): session.clear()
return None return None
conn.search(conn.user, encode_filter(current_app.config["LDAP_USER_SEARCH_FILTER"]))
if len(conn.entries) != 1: try:
user = User.query.get(dn)
if user:
return user
except LDAPInvalidDnError:
return None return None
return User.query.get(dn)
@bp.route("/logout") @bp.route("/logout")
def logout(): def logout():
...@@ -50,9 +43,12 @@ def logout(): ...@@ -50,9 +43,12 @@ def logout():
session.clear() session.clear()
return resp return resp
def set_session(user, skip_mfa=False): def set_session(user, password='', skip_mfa=False):
session.clear() session.clear()
session['user_dn'] = user.dn session['user_dn'] = user.dn
# only save the password if we use a user connection
if password and not current_app.config['LDAP_SERVICE_BIND_DN']:
session['user_pw'] = password
session['logintime'] = datetime.datetime.now().timestamp() session['logintime'] = datetime.datetime.now().timestamp()
session['_csrf_token'] = secrets.token_hex(128) session['_csrf_token'] = secrets.token_hex(128)
if skip_mfa: if skip_mfa:
...@@ -82,7 +78,7 @@ def login(): ...@@ -82,7 +78,7 @@ def login():
if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']): if not user.is_in_group(current_app.config['ACL_SELFSERVICE_GROUP']):
flash('You do not have access to this service') flash('You do not have access to this service')
return render_template('login.html', ref=request.values.get('ref')) return render_template('login.html', ref=request.values.get('ref'))
set_session(user) set_session(user, password=password)
return redirect(url_for('mfa.auth', ref=request.values.get('ref', url_for('index')))) return redirect(url_for('mfa.auth', ref=request.values.get('ref', url_for('index'))))
def get_current_user(): def get_current_user():
......
...@@ -97,6 +97,6 @@ def signup_confirm_submit(token): ...@@ -97,6 +97,6 @@ def signup_confirm_submit(token):
return render_template('signup/confirm.html', signup=signup, error=msg) return render_template('signup/confirm.html', signup=signup, error=msg)
db.session.commit() db.session.commit()
ldap.session.commit() ldap.session.commit()
set_session(user, skip_mfa=True) set_session(user, password=request.form['password'], skip_mfa=True)
flash('Your account was successfully created') flash('Your account was successfully created')
return redirect(url_for('selfservice.index')) return redirect(url_for('selfservice.index'))