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 }}:
-
-
- # |
- LDAP Username |
- Months Due |
- Payment Policy |
-
- {% for member in group.list %}
-
- {{loop.index}}. |
-
-
- {{member.ldap_username}}
-
- |
-
-
- {{member.months_due}}
-
- |
- {% include "button_payment_policy.html" %} |
-
- {% endfor %}
-
-
- {% endfor %}
-
-
Inactive members:
-
-
- # |
- LDAP Username |
- Months Due |
- Payment Policy |
-
- {% for member in inactive_members %}
-
- {{loop.index}}. |
-
-
- {{member.ldap_username}}
-
- |
-
-
- {{member.months_due}}
-
- |
- {% include "button_payment_policy.html" %} |
-
- {% endfor %}
-
-
+
+
+ {% for group in active_members|groupby("type") %}
+
+
Active members, {{ group.grouper }}:
+
+
+ # |
+ LDAP Username |
+ Months Due |
+ Payment Policy |
+
+ {% for member in group.list %}
+
+ {{loop.index}}. |
+
+
+ {{member.username}}
+
+ |
+
+
+ {{member.months_due}}
+
+ |
+ {% include "button_payment_policy.html" %} |
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+
Inactive members:
+
+
+ # |
+ LDAP Username |
+ Months Due |
+ Payment Policy |
+
+ {% for member in inactive_members %}
+
+ {{loop.index}}. |
+
+
+ {{member.username}}
+
+ |
+
+
+ {{member.months_due}}
+
+ |
+ {% include "button_payment_policy.html" %} |
+
+ {% endfor %}
+
+
+
+
{% 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 %}
+
+
+
+
+
+
+ {% if not form %}
+
+
No sync required - groups are up to date.
+
+ {% else %}
+
+ {% 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.get_contact_email() | 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()