From 14ab58f38a6044a794cec329688a7eb96d6b560f Mon Sep 17 00:00:00 2001 From: Remigiusz Marcinkiewicz Date: Sun, 12 Feb 2017 04:26:43 +0100 Subject: [PATCH] Updated LDAP integration - usernames are now LDAP usernames, added membership group sync --- web/webapp/directory.py | 44 +++++++ web/webapp/forms.py | 12 +- web/webapp/models.py | 12 +- web/webapp/templates/admin_index.html | 117 +++++++++--------- web/webapp/templates/admin_ldap_sync.html | 58 +++++++++ web/webapp/templates/admin_member.html | 6 +- .../templates/button_membership_type.html | 6 +- .../templates/button_payment_policy.html | 8 +- web/webapp/templates/memberlist.html | 2 +- web/webapp/views.py | 56 +++++++-- 10 files changed, 238 insertions(+), 83 deletions(-) create mode 100644 web/webapp/templates/admin_ldap_sync.html diff --git a/web/webapp/directory.py b/web/webapp/directory.py index fb281fd..16a4040 100644 --- a/web/webapp/directory.py +++ b/web/webapp/directory.py @@ -47,6 +47,50 @@ def _setup_ldap(): def _destroy_ldap(exception=None): g.ldap.unbind_s() +def get_ldap_group_diff(members): + active_members = filter(lambda m: m['judgement'], members) + fatty = set([member['username'] for member in active_members if member['type'] in ['fatty', 'supporting']]) + starving = set([member['username'] for member in active_members if member['type'] in ['starving']]) + + ldap_fatty = set(get_group_members(g.ldap, 'fatty')) + ldap_starving = set(get_group_members(g.ldap, 'starving')) + ldap_potato = set(get_group_members(g.ldap, 'potato')) + + result = {} + result['fatty_to_remove'] = list(ldap_fatty - fatty) + result['fatty_to_add'] = list(fatty - ldap_fatty) + result['starving_to_remove'] = list(ldap_starving - starving) + result['starving_to_add'] = list(starving - ldap_starving) + if sum([len(result[k]) for k in result]) == 0: + return None + return result + +def update_member_groups(c, changes): + ops = {'add': ldap.MOD_ADD, 'remove': ldap.MOD_DELETE} + for group in changes: + modlist = [] + for op in changes[group]: + for username in changes[group][op]: + modlist.append((ops[op],'uniqueMember','uid={},{}'.format(username.encode('utf-8'),app.config['LDAP_USER_BASE']))) + c.modify_s('cn={},{}'.format(group.encode('utf-8'),app.config['LDAP_GROUP_BASE']), modlist) + #print group, modlist + +def get_group_members(c, group): + lfilter = '(&(cn={}){})'.format(group, app.config['LDAP_GROUP_FILTER']) + data = c.search_s(app.config['LDAP_GROUP_BASE'], ldap.SCOPE_SUBTREE, + lfilter, tuple(['uniqueMember',])) + + members = [] + for dn, obj in data: + for k, v in obj.iteritems(): + if k == "uniqueMember": + for iv in v: + part,uid,index = ldap.dn.str2dn(iv)[0][0] + if not part == 'uid' or not index == 1: + raise ValueError("First part type {} or index {} seem wrong for DN {}".format(part,index,iv)) + members.append(uid.decode('utf-8')) + return members + def get_member_fields(c, member, fields): if isinstance(fields, str): fields = [fields,] diff --git a/web/webapp/forms.py b/web/webapp/forms.py index ad5e908..97dae07 100644 --- a/web/webapp/forms.py +++ b/web/webapp/forms.py @@ -22,8 +22,11 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from wtforms import Form, TextField, PasswordField, validators +from wtforms import Form, TextField, PasswordField, SelectMultipleField, FormField, validators, widgets +class MultiCheckboxField(SelectMultipleField): + widget = widgets.ListWidget(prefix_label=False) + option_widget = widgets.CheckboxInput() class LoginForm(Form): username = TextField('Username', [validators.Required()]) @@ -33,3 +36,10 @@ class LoginForm(Form): class BREFetchForm(Form): identifier = TextField("Identifier", [validators.Required()]) token = PasswordField("Identifier", [validators.Required()]) + +class LDAPSyncForm(Form): + fatty_to_add = MultiCheckboxField("Fatty to add", choices=[]) + fatty_to_remove = MultiCheckboxField("Fatty to remove", choices=[]) + starving_to_add = MultiCheckboxField("Starving to add", choices=[]) + starving_to_remove = MultiCheckboxField("Starving to remove", choices=[]) + diff --git a/web/webapp/models.py b/web/webapp/models.py index b415401..1bff65c 100644 --- a/web/webapp/models.py +++ b/web/webapp/models.py @@ -32,6 +32,7 @@ import json import re from sqlalchemy.orm import subqueryload_all +from sqlalchemy.sql.expression import or_ from flask import g from webapp import app, db, mc, cache_enabled @@ -88,7 +89,7 @@ class Member(db.Model): api_keys = db.relationship("APIKey") join_year = db.Column(db.Integer) join_month = db.Column(db.Integer) - ldap_username = db.Column(db.String(64), unique=True) + alias = db.Column(db.String(64)) # Normal - standard 3 months grace period # Extended Grace Period - do not shut off account after grace period # Potato - do not ever shut off account, report falsified payment status @@ -148,8 +149,8 @@ class Member(db.Model): del now_date status = {} - status['ldap_username'] = self.ldap_username status['username'] = self.username + status['alias'] = self.alias status['type'] = self.type status['payment_policy'] = self.payment_policy # First check - did we actually get any transfers? @@ -236,18 +237,18 @@ class Member(db.Model): def get_list_email(self): if self.preferred_email: return self.preferred_email - return '{}@hackerspace.pl'.format(self.ldap_username) + return '{}@hackerspace.pl'.format(self.username) def get_contact_email(self): if self.preferred_email: return self.preferred_email - mra = directory.get_member_fields(g.ldap, self.ldap_username, + mra = directory.get_member_fields(g.ldap, self.username, 'mailRoutingAddress') mra = mra['mailRoutingAddress'] if mra: return mra else: - return '{}@hackerspace.pl'.format(self.ldap_username) + return '{}@hackerspace.pl'.format(self.username) def get_status(self): @@ -302,7 +303,6 @@ class Member(db.Model): now_date = datetime.datetime.now() self.join_year = now_date.year self.join_month = now_date.month - self.ldap_username = _username self.payment_policy = PaymentPolicy.normal.value diff --git a/web/webapp/templates/admin_index.html b/web/webapp/templates/admin_index.html index 7ab7acd..ddfecaf 100644 --- a/web/webapp/templates/admin_index.html +++ b/web/webapp/templates/admin_index.html @@ -16,64 +16,69 @@ --> Fetch transfer data + Synchronize LDAP groups

- {% for group in active_members|groupby("type") %} -
-

Active members, {{ group.grouper }}:

- - - - - - - - {% for member in group.list %} - - - - - - - {% endfor %} -
#LDAP UsernameMonths DuePayment Policy
{{loop.index}}. - - {{member.ldap_username}} - - - - {{member.months_due}} - - {% include "button_payment_policy.html" %}
-
- {% endfor %} -
-

Inactive members:

- - - - - - - - {% for member in inactive_members %} - - - - - - - {% endfor %} -
#LDAP UsernameMonths DuePayment Policy
{{loop.index}}. - - {{member.ldap_username}} - - - - {{member.months_due}} - - {% include "button_payment_policy.html" %}
-
+
+
+ {% for group in active_members|groupby("type") %} +
+

Active members, {{ group.grouper }}:

+ + + + + + + + {% for member in group.list %} + + + + + + + {% endfor %} +
#LDAP UsernameMonths DuePayment Policy
{{loop.index}}. + + {{member.username}} + + + + {{member.months_due}} + + {% include "button_payment_policy.html" %}
+
+ {% endfor %} +
+

Inactive members:

+ + + + + + + + {% for member in inactive_members %} + + + + + + + {% endfor %} +
#LDAP UsernameMonths DuePayment Policy
{{loop.index}}. + + {{member.username}} + + + + {{member.months_due}} + + {% include "button_payment_policy.html" %}
+
+
+
{% endblock %} diff --git a/web/webapp/templates/admin_ldap_sync.html b/web/webapp/templates/admin_ldap_sync.html new file mode 100644 index 0000000..d624547 --- /dev/null +++ b/web/webapp/templates/admin_ldap_sync.html @@ -0,0 +1,58 @@ +{% extends "root.html" %} +{% set active_page = "admin" %} +{% block title %}Admin LDAP Sync{% endblock %} +{% block content %} + +
+
+
+

Active operations:

+

Available operations:

+

+ + Fetch transfer data + Synchronize LDAP groups +

+
+
+
+ {% if not form %} +
+

No sync required - groups are up to date.

+
+ {% else %} +
+
+ +
+
+

Fatty to add:

+ {{ form.fatty_to_add() }} +
+
+

Fatty to remove:

+ {{ form.fatty_to_remove() }} +
+
+

Starving to add:

+ {{ form.starving_to_add() }} +
+
+

Starving to remove:

+ {{ form.starving_to_remove() }} +
+
+ +
+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/web/webapp/templates/admin_member.html b/web/webapp/templates/admin_member.html index ca44fa3..d014fe4 100644 --- a/web/webapp/templates/admin_member.html +++ b/web/webapp/templates/admin_member.html @@ -5,13 +5,13 @@ {% set active_page = "profile" %} {% endif %} -{% block title %}{% if admin %}{{member.ldap_username}}{%else%}Profile{%endif%}{% endblock %} +{% block title %}{% if admin %}{{member.username}}{%else%}Profile{%endif%}{% endblock %} {% block content %}
gravatar -

{{member.ldap_username}}{%if cn %}
{{cn}}{% endif%}

+

{{member.username}}{%if cn %}
{{cn}}{% endif%}

{{member.get_contact_email()}}

{% if status.judgement %} @@ -62,7 +62,7 @@ (which is overdue), so is INACTIVE. {% endif %} {% else %} - . {{member.ldap_username}} Is in Extended Grace Period, so is ACTIVE. + . {{member.username}} Is in Extended Grace Period, so is ACTIVE. {% endif %} {% endif %} {% endif %} diff --git a/web/webapp/templates/button_membership_type.html b/web/webapp/templates/button_membership_type.html index 0eb74a5..8755de8 100644 --- a/web/webapp/templates/button_membership_type.html +++ b/web/webapp/templates/button_membership_type.html @@ -7,8 +7,8 @@ {{member.type|capitalize}}

diff --git a/web/webapp/templates/button_payment_policy.html b/web/webapp/templates/button_payment_policy.html index 73a7c9f..ad0b0aa 100644 --- a/web/webapp/templates/button_payment_policy.html +++ b/web/webapp/templates/button_payment_policy.html @@ -9,9 +9,9 @@ {{member.payment_policy}}
diff --git a/web/webapp/templates/memberlist.html b/web/webapp/templates/memberlist.html index 94621da..59869a9 100644 --- a/web/webapp/templates/memberlist.html +++ b/web/webapp/templates/memberlist.html @@ -17,7 +17,7 @@ {% for member in active_members %} - {{member['ldap_username']}} + {{member['username']}} {{member['type']}} {{member['joined'][0]}}/{{"%02i" |format(member['joined'][1])}} diff --git a/web/webapp/views.py b/web/webapp/views.py index eb39e60..b237ad8 100644 --- a/web/webapp/views.py +++ b/web/webapp/views.py @@ -64,11 +64,11 @@ def memberlist(): @app.route('/profile') @login_required def self_profile(): - member = models.Member.get_members(True).filter_by(ldap_username=current_user.username).first() + member = models.Member.get_members(True).filter_by(username=current_user.username).first() if not member: abort(404) status = member.get_status() - cn = directory.get_member_fields(g.ldap, member.ldap_username, 'cn')['cn'] + cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn'] return render_template("admin_member.html", member=member, status=status, cn=cn, admin=False) @@ -88,18 +88,56 @@ def admin_index(): active_members = filter(lambda m: m['judgement'], members) inactive_members = filter(lambda m: not m['judgement'], members) - + diff = directory.get_ldap_group_diff(members) + if diff is not None: + flash("LDAP sync required") return render_template("admin_index.html", active_members=active_members, inactive_members=inactive_members) +@app.route("/admin/ldapsync", methods=["POST", "GET"]) +@admin_required +@login_required +def admin_ldap_sync(): + members = [m.get_status() for m in models.Member.get_members(True)] + diff = directory.get_ldap_group_diff(members) + if diff is None: + return render_template("admin_ldap_sync.html", form=False) + + form = forms.LDAPSyncForm(request.form) + + form.fatty_to_add.choices = zip(diff['fatty_to_add'],diff['fatty_to_add']) + form.fatty_to_add.default = diff['fatty_to_add'] + + form.fatty_to_remove.choices = zip(diff['fatty_to_remove'],diff['fatty_to_remove']) + form.fatty_to_remove.default = diff['fatty_to_remove'] + + form.starving_to_add.choices = zip(diff['starving_to_add'],diff['starving_to_add']) + form.starving_to_add.default = diff['starving_to_add'] + + form.starving_to_remove.choices = zip(diff['starving_to_remove'],diff['starving_to_remove']) + form.starving_to_remove.default = diff['starving_to_remove'] + + form.process(request.form) + if request.method == "POST" and form.validate(): + changes = {'fatty': {}, 'starving': {}} + changes['fatty']['add'] = form.fatty_to_add.data + changes['fatty']['remove'] = form.fatty_to_remove.data + changes['starving']['add'] = form.starving_to_add.data + changes['starving']['remove'] = form.starving_to_remove.data + + directory.update_member_groups(g.ldap, changes) + + return render_template("admin_ldap_sync.html", form=form) + + @app.route("/admin/csv") @admin_required @login_required def admin_csv(): members = [m.get_status() for m in models.Member.get_members(True)] for member in members: - member["cn"] = directory.get_member_fields(g.ldap, member.ldap_username, 'cn')['cn'] + member["cn"] = directory.get_member_fields(g.ldap, member['username'], 'cn')['cn'] active_members = filter(lambda m: m['judgement'] and not m['type'] == 'supporting', members) @@ -110,11 +148,11 @@ def admin_csv(): @login_required @admin_required def admin_member(membername): - member = models.Member.get_members(True).filter_by(ldap_username=membername).first() + member = models.Member.get_members(True).filter_by(username=membername).first() if not member: abort(404) status = member.get_status() - cn = directory.get_member_fields(g.ldap, member.ldap_username, 'cn')['cn'] + cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn'] return render_template("admin_member.html", member=member, status=status, cn=cn, admin=True) @@ -122,7 +160,7 @@ def admin_member(membername): @login_required @admin_required def admin_member_set_policy(membername,policy): - member = models.Member.query.filter_by(ldap_username=membername).first() + member = models.Member.query.filter_by(username=membername).first() member.payment_policy = models.PaymentPolicy[policy].value db.session.add(member) db.session.commit() @@ -132,7 +170,7 @@ def admin_member_set_policy(membername,policy): @login_required @admin_required def admin_member_set_membership(membername,membershiptype): - member = models.Member.query.filter_by(ldap_username=membername).first() + member = models.Member.query.filter_by(username=membername).first() member.type = models.MembershipType[membershiptype].name db.session.add(member) db.session.commit() @@ -168,7 +206,7 @@ def admin_fetch(): except Exception as e: exc_type, exc_value, exc_traceback = sys.exc_info() - flash("Error when fetching data. %s" % traceback.format_exception(exc_type, exc_value,exc_traceback)) + flash("Error when fetching data.
%s
" % traceback.format_exception(exc_type, exc_value,exc_traceback)) return redirect(url_for("admin_fetch")) logic.update_transfer_rows()