diff --git a/app/models/users.py b/app/models/users.py index 80da7e1..116ec16 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -9,6 +9,8 @@ from app.models.notification import Notification import app.utils.unicode_names as unicode_names from app.utils.notify import notify from config import V5Config +from local_config import USE_LDAP +import app.utils.ldap as ldap import werkzeug.security import re @@ -108,7 +110,9 @@ class Member(User): self.name = name self.norm = unicode_names.normalize(name) self.email = email - self.set_password(password) + if not USE_LDAP: + self.set_password(password) + # Workflow with LDAP enabled is User → Postgresql → LDAP → set password self.xp = 0 self.bio = "" @@ -163,8 +167,11 @@ class Member(User): data = {key: data[key] for key in data if data[key] is not None} # TODO: verify good type of those args, think about the password mgt + # Beware of LDAP injections if "email" in data: self.email = data["email"] + if USE_LDAP: + ldap.set_email(self.norm, self.email) if "password" in data: self.set_password(data["password"]) if "bio" in data: @@ -203,13 +210,19 @@ class Member(User): Set the user's password. Check whether the request sender has the right to do this! """ - self.password_hash = werkzeug.security.generate_password_hash(password, - method='pbkdf2:sha512', salt_length=10) + if USE_LDAP: + ldap.set_password(self, password) + else: + self.password_hash = werkzeug.security.generate_password_hash( + password, method='pbkdf2:sha512', salt_length=10) def check_password(self, password): """Compares password against member hash.""" - return werkzeug.security.check_password_hash(self.password_hash, - password) + if USE_LDAP: + return ldap.check_password(self, password) + else: + return werkzeug.security.check_password_hash(self.password_hash, + password) def notify(self, message, href=None): """ Notify a user with a message. diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 8c9fb17..f02f8fb 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -4,6 +4,8 @@ from app import app, db from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm from app.models.users import Member from app.utils.render import render +import app.utils.ldap as ldap +from local_config import USE_LDAP @app.route('/account', methods=['GET', 'POST']) @@ -60,6 +62,10 @@ def register(): member.newsletter = form.newsletter.data db.session.add(member) db.session.commit() + # Workflow with LDAP is User → Postgresql → LDAP → Change password + if USE_LDAP: + ldap.add_member(member) + ldap.set_password(member, form.password.data) flash('Inscription réussie', 'ok') return redirect(url_for('validation') + "?email=" + form.email.data) return render('register.html', title='Register', form=form) diff --git a/app/routes/account/login.py b/app/routes/account/login.py index c63bce9..121fc5a 100644 --- a/app/routes/account/login.py +++ b/app/routes/account/login.py @@ -1,6 +1,6 @@ from flask import redirect, url_for, request, flash from flask_login import login_user, logout_user, login_required, current_user -from urllib.parse import urlparse +from urllib.parse import urlparse, urljoin from app import app from app.forms.login import LoginForm from app.models.users import Member diff --git a/app/utils/ldap.py b/app/utils/ldap.py new file mode 100644 index 0000000..1b67474 --- /dev/null +++ b/app/utils/ldap.py @@ -0,0 +1,83 @@ +import ldap +from ldap.modlist import addModlist, modifyModlist +from local_config import LDAP_PASSWORD, LDAP_ORGANIZATION + + +def get_member(username): + """ Get informations about member. Username must be normalized! """ + conn = ldap.initialize("ldap://localhost") + # Search for user + r = conn.search_s(LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE, f'(cn={username})') + if len(r) > 0: + return r[0] + else: + return None + + +def edit(user, fields): + """ Edit a user. Fields is {'name': ['value'], …} """ + conn = ldap.initialize("ldap://localhost") + # Connect as root + # conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD) + # old_value = {"userPassword": ["my_old_password"]} + # new_value = {"userPassword": ["my_new_password"]} + + modlist = ldap.modlist.modifyModlist(old_value, new_value) + con.modify_s(dn, modlist) + + +def set_email(user, email): + pass + + +def set_password(user, password): + """ Set password for a user. """ + conn = ldap.initialize("ldap://localhost") + # Connect as root + conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', + LDAP_PASSWORD) + conn.passwd_s(f"cn={user.norm},{LDAP_ORGANIZATION}", None, password) + + +def check_password(user, password): + """ Try to login a user through LDAP register. """ + conn = ldap.initialize("ldap://localhost") + try: + conn.simple_bind_s(f"cn={user.norm},{LDAP_ORGANIZATION}", password) + except ldap.INVALID_CREDENTIALS: + return False + return True + + +def add_member(member): + """ Add a member to LDAP register. Fields must have been sanitized! """ + if get_member(member.norm) is not None: + print("User already exists") + return + conn = ldap.initialize("ldap://localhost") + # Connect as root + conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD) + # Create fields + dn = f'cn={member.norm},{LDAP_ORGANIZATION}' + modlist = addModlist({ + 'objectClass': [bytes('inetOrgPerson', 'UTF8')], + 'cn': [bytes(member.norm, 'UTF8')], + 'sn': [bytes(member.norm, 'UTF8')], + 'displayName': [bytes(member.name, 'UTF8')], + 'mail': [bytes(member.email, 'UTF8')], + 'uid': [bytes(str(member.id), 'UTF8')], + 'userPassword': [bytes("", 'UTF8')] + }) + # Add the member + conn.add_s(dn, modlist) + + +def delete_member(member): + """ Remove a member from LDAP register """ + conn = ldap.initialize("ldap://localhost") + # Connect as root + conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD) + # Create fields + dn = f'cn={member.norm},{LDAP_ORGANIZATION}' + # Delete the user + conn.delete_s(dn) diff --git a/app/utils/validators.py b/app/utils/validators.py index 1e8b1b7..9cc7056 100644 --- a/app/utils/validators.py +++ b/app/utils/validators.py @@ -3,7 +3,9 @@ from wtforms.validators import ValidationError from app.models.users import Member from app.utils.valid_name import valid_name from app.utils.unicode_names import normalize +import app.utils.ldap as ldap from config import V5Config +from local_config import USE_LDAP def name_valid(form, name): @@ -42,6 +44,13 @@ def name_available(form, name): if member is not None: raise ValidationError("Ce nom d'utilisateur est indisponible.") + # Double check with LDAP if needed + if USE_LDAP: + member = ldap.get_member(norm) + if member is not None: + raise ValidationError("Ce nom d'utilisateur est indisponible.") + + def email(form, email): member = Member.query.filter_by(email=email.data).first() diff --git a/local_config.py.default b/local_config.py.default index c6291c5..b6dfff2 100644 --- a/local_config.py.default +++ b/local_config.py.default @@ -1,3 +1,4 @@ DB_NAME = "pcv5" -LDAP_PASSWORD = "openldap" USE_LDAP = False +LDAP_PASSWORD = "openldap" +LDAP_ORGANIZATION = "o=planet-casio"