diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg index 95f0934817c38f404b93d8f79e465758403fbffb..55a8d577b296fa23638a5b6d4d915e67e129a6d1 100644 --- a/uffd/default_config.cfg +++ b/uffd/default_config.cfg @@ -5,12 +5,23 @@ LDAP_BASE_MAIL="ou=postfix,dc=example,dc=com" LDAP_SERVICE_BIND_DN="" LDAP_SERVICE_BIND_PASSWORD="" LDAP_SERVICE_URL="ldapi:///" +LDAP_SERVICE_USE_STARTTLS=True LDAP_USER_OBJECTCLASSES=["top", "inetOrgPerson", "organizationalPerson", "person", "posixAccount"] +LDAP_USER_ATTRIBUTE_UID="uidNumber" +LDAP_USER_ATTRIBUTE_DISPLAYNAME="cn" +LDAP_USER_ATTRIBUTE_MAIL="mail" +# The User class gets filled by which LDAP attribute and to type (single/list) +LDAP_USER_ATTRIBUTE_EXTRA={ +#"phone": {"type": "single", "name": "mobile"}, +} +LDAP_USER_FILTER="(objectClass=person)" LDAP_USER_GID=20001 LDAP_USER_MIN_UID=10000 LDAP_USER_MAX_UID=18999 +LDAP_GROUP_FILTER='(objectClass=groupOfUniqueNames)' + SESSION_LIFETIME_SECONDS=3600 # CSRF protection SESSION_COOKIE_SECURE=True diff --git a/uffd/ldap/ldap.py b/uffd/ldap/ldap.py index 880e86ad0984bcccc99cd06d06037ac315158f9d..e1fbf5ac759e4bba4d45fccf5e8b34ef162b406a 100644 --- a/uffd/ldap/ldap.py +++ b/uffd/ldap/ldap.py @@ -4,7 +4,7 @@ from flask import Blueprint, current_app from ldap3.utils.conv import escape_filter_chars from ldap3.core.exceptions import LDAPBindError, LDAPCursorError, LDAPPasswordIsMandatoryError -from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MOCK_SYNC +from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, MOCK_SYNC, AUTO_BIND_TLS_BEFORE_BIND bp = Blueprint("ldap", __name__) @@ -33,7 +33,9 @@ def service_conn(): if current_app.config.get('LDAP_SERVICE_MOCK', False): return get_mock_conn() server = Server(current_app.config["LDAP_SERVICE_URL"], get_info=ALL) - return fix_connection(Connection(server, current_app.config["LDAP_SERVICE_BIND_DN"], current_app.config["LDAP_SERVICE_BIND_PASSWORD"], auto_bind=True)) + return fix_connection(Connection(server, current_app.config["LDAP_SERVICE_BIND_DN"], + current_app.config["LDAP_SERVICE_BIND_PASSWORD"], + auto_bind=AUTO_BIND_TLS_BEFORE_BIND if current_app.config["LDAP_SERVICE_USE_STARTTLS"] else True)) def user_conn(loginname, password): if not loginname_is_safe(loginname): @@ -53,7 +55,8 @@ def user_conn(loginname, password): return get_mock_conn() server = Server(current_app.config["LDAP_SERVICE_URL"], get_info=ALL) try: - return fix_connection(Connection(server, loginname_to_dn(loginname), password, auto_bind=True)) + return fix_connection(Connection(server, loginname_to_dn(loginname), password, + auto_bind=AUTO_BIND_TLS_BEFORE_BIND if current_app.config["LDAP_SERVICE_USE_STARTTLS"] else True)) except (LDAPBindError, LDAPPasswordIsMandatoryError): return False @@ -62,7 +65,8 @@ def get_conn(): def uid_to_dn(uid): conn = get_conn() - conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format(escape_filter_chars(uid))) + conn.search(current_app.config["LDAP_BASE_USER"], + '(&{}(uidNumber={}))'.format(current_app.config["LDAP_USER_FILTER"], escape_filter_chars(uid))) if not len(conn.entries) == 1: return None return conn.entries[0].entry_dn @@ -90,7 +94,7 @@ def mailname_is_safe(value): def get_next_uid(): conn = get_conn() - conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)') + conn.search(current_app.config["LDAP_BASE_USER"], current_app.config["LDAP_USER_FILTER"]) max_uid = current_app.config["LDAP_USER_MIN_UID"] for i in conn.entries: # skip out of range entries @@ -106,7 +110,7 @@ def get_next_uid(): def get_ldap_attribute_safe(ldapobject, attribute): try: - result = ldapobject[attribute].value if attribute in ldapobject else None + result = ldapobject[attribute].value if attribute in ldapobject else None # we have to catch LDAPCursorError here, because ldap3 in older versions has a broken __contains__ function # see https://github.com/cannatag/ldap3/issues/493 # fixed in version 2.5 diff --git a/uffd/session/views.py b/uffd/session/views.py index 50eaecea1301d745fb42f41fe3f949ad0d2a0585..5b2d9bef52453989beaa8c80df3b9218d9748b26 100644 --- a/uffd/session/views.py +++ b/uffd/session/views.py @@ -37,7 +37,7 @@ def login(): return render_template('login.html', ref=request.values.get('ref')) conn = user_conn(username, password) if conn: - conn.search(conn.user, '(objectClass=person)') + conn.search(conn.user, current_app.config["LDAP_USER_FILTER"]) if not conn or len(conn.entries) != 1: login_ratelimit.log(username) host_ratelimit.log() diff --git a/uffd/user/models.py b/uffd/user/models.py index 3cf845836fde50f872870ddd3d470625656dfa41..294cdb231cd959e9209eb581735724cb5854b29a 100644 --- a/uffd/user/models.py +++ b/uffd/user/models.py @@ -6,15 +6,20 @@ from flask import current_app from uffd import ldap -class User(): - def __init__(self, uid=None, loginname='', displayname='', mail='', groups=None, dn=None): - self.uid = uid - self.loginname = loginname - self.displayname = displayname - self.mail = mail - self.newpassword = None - self.dn = dn +class BaseUser: + def __init__(self, attributes=None, groups=None, dn=None): + self.uid = None + self.mail = '' + self.loginname = '' + self.displayname = '' + + if attributes is not None: + for attribute_name, attribute_value in attributes.items(): + setattr(self, attribute_name, attribute_value) + + self.dn = dn + self.newpassword = None self.groups_ldap = groups or [] self.initial_groups_ldap = groups or [] self.groups_changed = False @@ -22,19 +27,30 @@ class User(): @classmethod def from_ldap(cls, ldapobject): + ldap_attributes = { + "loginname": ldap.get_ldap_attribute_safe(ldapobject, "uid"), + "uid": ldap.get_ldap_attribute_safe(ldapobject, current_app.config["LDAP_USER_ATTRIBUTE_UID"]), + "displayname": ldap.get_ldap_attribute_safe(ldapobject, current_app.config["LDAP_USER_ATTRIBUTE_DISPLAYNAME"]), + "mail": ldap.get_ldap_attribute_safe(ldapobject, current_app.config["LDAP_USER_ATTRIBUTE_MAIL"]), + } + + for user_attribute, ldap_attribute in current_app.config["LDAP_USER_ATTRIBUTE_EXTRA"].items(): + ldap_attribute_name = ldap_attribute.get("name", "") + if ldap_attribute.get("type", "single"): + ldap_attributes[user_attribute] = ldap.get_ldap_attribute_safe(ldapobject, ldap_attribute_name) + else: + ldap_attributes[user_attribute] = ldap.get_ldap_array_attribute_safe(ldapobject, ldap_attribute_name) + return User( - uid=ldapobject['uidNumber'].value, - loginname=ldapobject['uid'].value, - displayname=ldapobject['cn'].value, - mail=ldapobject['mail'].value, groups=ldap.get_ldap_array_attribute_safe(ldapobject, 'memberOf'), dn=ldapobject.entry_dn, + attributes=ldap_attributes, ) @classmethod def from_ldap_dn(cls, dn): conn = ldap.get_conn() - conn.search(dn, '(objectClass=person)') + conn.search(dn, current_app.config["LDAP_USER_FILTER"]) if not len(conn.entries) == 1: return None return User.from_ldap(conn.entries[0]) @@ -44,7 +60,9 @@ class User(): if new: self.uid = ldap.get_next_uid() attributes = { - 'uidNumber': self.uid, + current_app.config["LDAP_USER_ATTRIBUTE_UID"]: self.uid, + current_app.config["LDAP_USER_ATTRIBUTE_DISPLAYNAME"]: self.displayname, + current_app.config["LDAP_USER_ATTRIBUTE_MAIL"]: self.mail, 'gidNumber': current_app.config['LDAP_USER_GID'], 'homeDirectory': '/home/'+self.loginname, 'sn': ' ', @@ -52,8 +70,6 @@ class User(): # same as for update 'givenName': self.displayname, 'displayName': self.displayname, - 'cn': self.displayname, - 'mail': self.mail, } dn = ldap.loginname_to_dn(self.loginname) result = conn.add(dn, current_app.config['LDAP_USER_OBJECTCLASSES'], attributes) @@ -61,8 +77,8 @@ class User(): attributes = { 'givenName': [(MODIFY_REPLACE, [self.displayname])], 'displayName': [(MODIFY_REPLACE, [self.displayname])], - 'cn': [(MODIFY_REPLACE, [self.displayname])], - 'mail': [(MODIFY_REPLACE, [self.mail])], + current_app.config["LDAP_USER_ATTRIBUTE_DISPLAYNAME"]: [(MODIFY_REPLACE, [self.displayname])], + current_app.config["LDAP_USER_ATTRIBUTE_MAIL"]: [(MODIFY_REPLACE, [self.mail])], } if self.newpassword: attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])] @@ -145,7 +161,11 @@ class User(): self.mail = value return True -class Group(): + +User = BaseUser + + +class Group: def __init__(self, gid=None, name='', members=None, description='', dn=None): self.gid = gid self.name = name @@ -167,7 +187,7 @@ class Group(): @classmethod def from_ldap_dn(cls, dn): conn = ldap.get_conn() - conn.search(dn, '(objectClass=groupOfUniqueNames)') + conn.search(dn, current_app.config["LDAP_GROUP_FILTER"]) if not len(conn.entries) == 1: return None return Group.from_ldap(conn.entries[0]) @@ -175,7 +195,7 @@ class Group(): @classmethod def from_ldap_all(cls): conn = ldap.get_conn() - conn.search(current_app.config["LDAP_BASE_GROUPS"], '(objectclass=groupOfUniqueNames)') + conn.search(current_app.config["LDAP_BASE_GROUPS"], current_app.config["LDAP_GROUP_FILTER"]) groups = [] for i in conn.entries: groups.append(Group.from_ldap(i)) diff --git a/uffd/user/views_group.py b/uffd/user/views_group.py index 8c1c805eeb00ca6fe43e054bb97aeb590b130df3..a1d89b244082b5ff22a930c8198c9349a564f287 100644 --- a/uffd/user/views_group.py +++ b/uffd/user/views_group.py @@ -25,7 +25,8 @@ def index(): @bp.route("/<int:gid>") def show(gid): conn = get_conn() - conn.search(current_app.config["LDAP_BASE_GROUPS"], '(&(objectclass=groupOfUniqueNames)(gidNumber={}))'.format((escape_filter_chars(gid)))) + conn.search(current_app.config["LDAP_BASE_GROUPS"], + '(&{}(gidNumber={}))'.format(current_app.config["LDAP_GROUP_FILTER"], escape_filter_chars(gid))) assert len(conn.entries) == 1 group = Group.from_ldap(conn.entries[0]) return render_template('group.html', group=group) diff --git a/uffd/user/views_user.py b/uffd/user/views_user.py index 77488a229f51fefec2cbe016b1380a9b83a85852..7e9d7f2f681d75bef81a114fb6c92cf963d912a9 100644 --- a/uffd/user/views_user.py +++ b/uffd/user/views_user.py @@ -29,7 +29,7 @@ def user_acl_check(): @register_navbar('Users', icon='users', blueprint=bp, visible=user_acl_check) def index(): conn = get_conn() - conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)') + conn.search(current_app.config["LDAP_BASE_USER"], current_app.config["LDAP_USER_FILTER"]) users = [] for i in conn.entries: users.append(User.from_ldap(i)) @@ -43,7 +43,8 @@ def show(uid=None): ldif = '<none yet>' else: conn = get_conn() - conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid)))) + conn.search(current_app.config["LDAP_BASE_USER"], + '(&{}(uidNumber={}))'.format(current_app.config["LDAP_USER_FILTER"], escape_filter_chars(uid))) assert len(conn.entries) == 1 user = User.from_ldap(conn.entries[0]) ldif = conn.entries[0].entry_to_ldif() @@ -61,7 +62,8 @@ def update(uid=False): flash('Login name does not meet requirements') return redirect(url_for('user.show')) else: - conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid)))) + conn.search(current_app.config["LDAP_BASE_USER"], + '(&{}(uidNumber={}))'.format(current_app.config["LDAP_USER_FILTER"], escape_filter_chars(uid))) assert len(conn.entries) == 1 user = User.from_ldap(conn.entries[0]) if not user.set_mail(request.form['mail']): @@ -106,7 +108,8 @@ def update(uid=False): @csrf_protect(blueprint=bp) def delete(uid): conn = get_conn() - conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid)))) + conn.search(current_app.config["LDAP_BASE_USER"], + '(&{}(uidNumber={}))'.format(current_app.config["LDAP_USER_FILTER"], escape_filter_chars(uid))) assert len(conn.entries) == 1 user = User.from_ldap(conn.entries[0])