From e4ffe3403a87d8984fb230dfe857584224bf6885 Mon Sep 17 00:00:00 2001 From: nd <git@notandy.de> Date: Sat, 19 Sep 2020 00:26:19 +0200 Subject: [PATCH] finished mail mapping editing support --- uffd/default_config.cfg | 3 ++ uffd/ldap/__init__.py | 2 +- uffd/ldap/ldap.py | 8 +++ uffd/mail/models.py | 39 +++------------ uffd/mail/templates/mail.html | 37 ++++++++++++++ uffd/mail/views.py | 92 ++++++++++------------------------- 6 files changed, 83 insertions(+), 98 deletions(-) create mode 100644 uffd/mail/templates/mail.html diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg index 1f93e78e..e7e27480 100644 --- a/uffd/default_config.cfg +++ b/uffd/default_config.cfg @@ -1,9 +1,11 @@ LDAP_BASE_USER="ou=users,dc=example,dc=com" LDAP_BASE_GROUPS="ou=groups,dc=example,dc=com" LDAP_BASE_MAIL="ou=postfix,dc=example,dc=com" + LDAP_SERVICE_BIND_DN="" LDAP_SERVICE_BIND_PASSWORD="" LDAP_SERVICE_URL="ldapi:///" + LDAP_USER_OBJECTCLASSES=["top", "inetOrgPerson", "organizationalPerson", "person", "posixAccount"] LDAP_USER_GID=20001 LDAP_USER_MIN_UID=10000 @@ -21,6 +23,7 @@ MAIL_USERNAME='yourId@gmail.com' MAIL_PASSWORD='*****' MAIL_USE_STARTTLS=True MAIL_FROM_ADDRESS='foo@bar.com' +MAIL_LDAP_OBJECTCLASSES=["top", "postfixVirtual"] ROLES_BASEROLES=['base'] diff --git a/uffd/ldap/__init__.py b/uffd/ldap/__init__.py index 436be193..26171f0d 100644 --- a/uffd/ldap/__init__.py +++ b/uffd/ldap/__init__.py @@ -1,6 +1,6 @@ from .ldap import bp as ldap_bp from .ldap import get_conn, user_conn, escape_filter_chars, uid_to_dn -from .ldap import loginname_to_dn, get_next_uid, loginname_is_safe +from .ldap import loginname_to_dn, mail_to_dn, get_next_uid, loginname_is_safe, mailname_is_safe from .ldap import get_ldap_array_attribute_safe, get_ldap_attribute_safe bp = [ldap_bp] diff --git a/uffd/ldap/ldap.py b/uffd/ldap/ldap.py index 39fee2ff..8380d946 100644 --- a/uffd/ldap/ldap.py +++ b/uffd/ldap/ldap.py @@ -44,6 +44,11 @@ def loginname_to_dn(loginname): return 'uid={},{}'.format(loginname, current_app.config["LDAP_BASE_USER"]) raise Exception('unsafe login name') +def mail_to_dn(uid): + if mailname_is_safe(uid): + return 'uid={},{}'.format(uid, current_app.config["LDAP_BASE_MAIL"]) + raise Exception('unsafe mail name') + def loginname_is_safe(value): if len(value) > 32 or len(value) < 1: return False @@ -52,6 +57,9 @@ def loginname_is_safe(value): return False return True +def mailname_is_safe(value): + return loginname_is_safe(value) + def get_next_uid(): conn = get_conn() conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)') diff --git a/uffd/mail/models.py b/uffd/mail/models.py index 1e404b1b..dd8d810b 100644 --- a/uffd/mail/models.py +++ b/uffd/mail/models.py @@ -32,42 +32,19 @@ class Mail(): def to_ldap(self, new=False): conn = ldap.get_conn() if new: - self.uid = ldap.get_next_uid() attributes = { - 'uidNumber': self.uid, - 'gidNumber': current_app.config['LDAP_USER_GID'], - 'homeDirectory': '/home/'+self.loginname, - 'sn': ' ', - 'userPassword': hashed(HASHED_SALTED_SHA512, secrets.token_hex(128)), + 'uid': self.uid, # same as for update - 'givenName': self.displayname, - 'displayName': self.displayname, - 'cn': self.displayname, - 'mail': self.mail, + 'mailacceptinggeneralid': self.receivers, + 'maildrop': self.destinations, } - dn = ldap.loginname_to_dn(self.loginname) - result = conn.add(dn, current_app.config['LDAP_USER_OBJECTCLASSES'], attributes) + self.dn = ldap.mail_to_dn(self.uid) + result = conn.add(self.dn, current_app.config['MAIL_LDAP_OBJECTCLASSES'], attributes) else: attributes = { - 'givenName': [(MODIFY_REPLACE, [self.displayname])], - 'displayName': [(MODIFY_REPLACE, [self.displayname])], - 'cn': [(MODIFY_REPLACE, [self.displayname])], - 'mail': [(MODIFY_REPLACE, [self.mail])], + 'mailacceptinggeneralid': [(MODIFY_REPLACE, self.receivers)], + 'maildrop': [(MODIFY_REPLACE, self.destinations)], } - if self.newpassword: - attributes['userPassword'] = [(MODIFY_REPLACE, [hashed(HASHED_SALTED_SHA512, self.newpassword)])] - dn = ldap.uid_to_dn(self.uid) - result = conn.modify(dn, attributes) - self.dn = dn - - group_conn = ldap.get_conn() - for group in self.initial_groups_ldap: - if not group in self.groups_ldap: - group_conn.modify(group, {'uniqueMember': [(MODIFY_DELETE, [self.dn])]}) - for group in self.groups_ldap: - if not group in self.initial_groups_ldap: - group_conn.modify(group, {'uniqueMember': [(MODIFY_ADD, [self.dn])]}) - self.groups_changed = False - + result = conn.modify(self.dn, attributes) return result diff --git a/uffd/mail/templates/mail.html b/uffd/mail/templates/mail.html new file mode 100644 index 00000000..36021697 --- /dev/null +++ b/uffd/mail/templates/mail.html @@ -0,0 +1,37 @@ +{% extends 'base.html' %} + +{% block body %} +<form action="{{ url_for("mail.update", uid=mail.uid) }}" method="POST"> +<div class="align-self-center"> + <div class="form-group col"> + <label for="mail-name">Name</label> + <input type="text" class="form-control" id="mail-name" name="mail-uid" {% if mail.uid %} value="{{ mail.uid }}" readonly {% else %} value=""{% endif %}> + <small class="form-text text-muted"> + </small> + </div> + <div class="form-group col"> + <label for="mail-receivers">Receiving addresses</label> + <textarea rows="10" class="form-control" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea> + <small class="form-text text-muted"> + One address per line + </small> + </div> + <div class="form-group col"> + <label for="mail-destinations">Destinations</label> + <textarea rows="10" class="form-control" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea> + <small class="form-text text-muted"> + One address per line + </small> + </div> + <div class="form-group col"> + <button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> Save</button> + <a href="{{ url_for("mail.index") }}" class="btn btn-secondary">Cancel</a> + {% if mail.uid %} + <a href="{{ url_for("mail.delete", uid=mail.uid) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> + {% else %} + <a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> + {% endif %} + </div> +</div> +</form> +{% endblock %} diff --git a/uffd/mail/views.py b/uffd/mail/views.py index 460ca53b..8ad6410d 100644 --- a/uffd/mail/views.py +++ b/uffd/mail/views.py @@ -32,92 +32,52 @@ def index(): @bp.route("/<uid>") @bp.route("/new") def show(uid=None): - return None if not uid: - user = User() - ldif = '<none yet>' + mail = Mail() 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_MAIL"], '(&(objectclass=postfixVirtual)(uid={}))'.format((escape_filter_chars(uid)))) assert len(conn.entries) == 1 - user = User.from_ldap(conn.entries[0]) - ldif = conn.entries[0].entry_to_ldif() - return render_template('user.html', user=user, user_ldif=ldif, roles=Role.query.all()) + mail = Mail.from_ldap(conn.entries[0]) + return render_template('mail.html', mail=mail) @bp.route("/<uid>/update", methods=['POST']) @bp.route("/new", methods=['POST']) @csrf_protect(blueprint=bp) def update(uid=False): - return None conn = get_conn() - is_newuser = bool(not uid) - if is_newuser: - user = User() - if not user.set_loginname(request.form['loginname']): - flash('Login name does not meet requirements') - return redirect(url_for('user.show')) + is_newmail = bool(not uid) + if is_newmail: + mail = Mail() else: - conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid)))) + conn = get_conn() + conn.search(current_app.config["LDAP_BASE_MAIL"], '(&(objectclass=postfixVirtual)(uid={}))'.format((escape_filter_chars(uid)))) assert len(conn.entries) == 1 - user = User.from_ldap(conn.entries[0]) - if not user.set_mail(request.form['mail']): - flash('Mail is invalide.') - return redirect(url_for('user.show', uid=uid)) - new_displayname = request.form['displayname'] if request.form['displayname'] else request.form['loginname'] - if not user.set_displayname(new_displayname): - flash('Display name does not meet requirements') - return redirect(url_for('user.show', uid=uid)) - new_password = request.form.get('password') - if new_password and not is_newuser: - user.set_password(new_password) + mail = Mail.from_ldap(conn.entries[0]) - session = db.session - roles = Role.query.all() - for role in roles: - role_member_dns = role.member_dns() - if request.values.get('role-{}'.format(role.id), False) or role.name in current_app.config["ROLES_BASEROLES"]: - if user.dn in role_member_dns: - continue - role.add_member(user) - elif user.dn in role_member_dns: - role.del_member(user) - usergroups = set() - for role in Role.get_for_user(user).all(): - usergroups.update(role.group_dns()) - user.replace_group_dns(usergroups) + if is_newmail: + mail.uid = request.form.get('mail-uid') + mail.receivers = request.form.get('mail-receivers', '').splitlines(); + mail.destinations = request.form.get('mail-destinations', '').splitlines(); - if user.to_ldap(new=is_newuser): - if is_newuser: - send_passwordreset(user.loginname) - flash('User created. We sent the user a password reset link by mail') - session.commit() - else: - flash('User updated') - session.commit() + if mail.to_ldap(new=is_newmail): + flash('Mail mapping updated.') else: - flash('Error updating user: {}'.format(conn.result['message'])) - session.rollback() - return redirect(url_for('user.show', uid=user.uid)) + flash('Error updating mail mapping: {}'.format(conn.result['message'])) + if is_newmail: + return redirect(url_for('mail.index')) + return redirect(url_for('mail.show', uid=mail.uid)) @bp.route("/<uid>/del") @csrf_protect(blueprint=bp) def delete(uid): - return None 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_MAIL"], '(&(objectclass=postfixVirtual)(uid={}))'.format((escape_filter_chars(uid)))) assert len(conn.entries) == 1 - user = User.from_ldap(conn.entries[0]) - - session = db.session - roles = Role.query.all() - for role in roles: - if user.dn in role.member_dns(): - role.del_member(user) + mail = conn.entries[0] - if conn.delete(conn.entries[0].entry_dn): - flash('Deleted user') - session.commit() + if conn.delete(mail.entry_dn): + flash('Deleted mail mapping.') else: - flash('Could not delete user: {}'.format(conn.result['message'])) - session.rollback() - return redirect(url_for('user.index')) + flash('Could not delete mail mapping: {}'.format(conn.result['message'])) + return redirect(url_for('mail.index')) -- GitLab