Updated LDAP integration - usernames are now LDAP usernames, added membership group sync

master
Remigiusz Marcinkiewicz 2017-02-12 04:26:43 +01:00 committed by Kasownik
parent 763fb95b52
commit 14ab58f38a
10 changed files with 238 additions and 83 deletions

View File

@ -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,]

View File

@ -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=[])

View File

@ -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

View File

@ -16,64 +16,69 @@
<button type="button" class="btn btn-primary">Send reminders</button>
</form>-->
<a href="/admin/fetch"><b>Fetch transfer data</b></a>
<a href="/admin/ldapsync"><b>Synchronize LDAP groups</b></a>
</p>
</div>
{% for group in active_members|groupby("type") %}
<div class="col-md-5">
<h2>Active members, {{ group.grouper }}:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in group.list %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.ldap_username}}">
<b>{{member.ldap_username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
<div class="col-md-5">
<h2>Inactive members:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in inactive_members %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.ldap_username}}">
<b>{{member.ldap_username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="col-md-10">
<div class="row">
{% for group in active_members|groupby("type") %}
<div class="col-md-6">
<h2>Active members, {{ group.grouper }}:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in group.list %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.username}}">
<b>{{member.username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
<div class="col-md-6">
<h2>Inactive members:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in inactive_members %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.username}}">
<b>{{member.username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,58 @@
{% extends "root.html" %}
{% set active_page = "admin" %}
{% block title %}Admin LDAP Sync{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-2 operations">
<h4>Active operations:</h4>
<h4>Available operations:</h4>
<p>
<!--<form action="/fetch" method="post">
<button type="button" class="btn btn-primary">Fetch transfer data</button>
</form>
<form action="/spam" method="post">
<button type="button" class="btn btn-primary">Send reminders</button>
</form>-->
<a href="/admin/fetch"><b>Fetch transfer data</b></a>
<a href="/admin/ldapsync"><b>Synchronize LDAP groups</b></a>
</p>
</div>
<div class="col-md-10">
<div class="row">
{% if not form %}
<div class="col-md-12">
<h2>No sync required - groups are up to date.</h2>
</div>
{% else %}
<form method="post" action="/admin/ldapsync">
<div class="col-md-12">
<input type="submit" value="Sync" />
</div>
<div class="col-md-3">
<h2>Fatty to add:</h2>
{{ form.fatty_to_add() }}
</div>
<div class="col-md-3">
<h2>Fatty to remove:</h2>
{{ form.fatty_to_remove() }}
</div>
<div class="col-md-3">
<h2>Starving to add:</h2>
{{ form.starving_to_add() }}
</div>
<div class="col-md-3">
<h2>Starving to remove:</h2>
{{ form.starving_to_remove() }}
</div>
<div class="col-md-12">
<input type="submit" value="Sync" />
</div>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -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 %}
<div class="container">
<div class="row">
<div class="col-md-3">
<img src="{{ member.get_contact_email() | gravatar }}" alt="gravatar" style="width: 70%; margin-top: 10px;" />
<h1>{{member.ldap_username}}{%if cn %}<br /><small>{{cn}}</small>{% endif%}</h1>
<h1>{{member.username}}{%if cn %}<br /><small>{{cn}}</small>{% endif%}</h1>
<h5>{{member.get_contact_email()}}</h5>
<p>
{% if status.judgement %}
@ -62,7 +62,7 @@
(which is <b>overdue</b>), so is <b>INACTIVE</b>.
{% endif %}
{% else %}
. {{member.ldap_username}} Is in Extended Grace Period, so is <b>ACTIVE</b>.
. {{member.username}} Is in Extended Grace Period, so is <b>ACTIVE</b>.
{% endif %}
{% endif %}
{% endif %}

View File

@ -7,8 +7,8 @@
{{member.type|capitalize}} <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="/admin/member/{{member.ldap_username}}/membership:fatty">Fatty (100PLN p/m)</a></li>
<li><a href="/admin/member/{{member.ldap_username}}/membership:starving">Starving (50PLN p/m)</a></li>
<li><a href="/admin/member/{{member.ldap_username}}/membership:supporting">Supporting</a></li>
<li><a href="/admin/member/{{member.username}}/membership:fatty">Fatty (100PLN p/m)</a></li>
<li><a href="/admin/member/{{member.username}}/membership:starving">Starving (50PLN p/m)</a></li>
<li><a href="/admin/member/{{member.username}}/membership:supporting">Supporting</a></li>
</ul>
</div>

View File

@ -9,9 +9,9 @@
{{member.payment_policy}} <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="/admin/member/{{member.ldap_username}}/policy:normal">Normal</a></li>
<li><a href="/admin/member/{{member.ldap_username}}/policy:extended">Extended Grace Period</a></li>
<li><a href="/admin/member/{{member.ldap_username}}/policy:potato">Potato</a></li>
<li><a href="/admin/member/{{member.ldap_username}}/policy:disabled">Disabled</a></li>
<li><a href="/admin/member/{{member.username}}/policy:normal">Normal</a></li>
<li><a href="/admin/member/{{member.username}}/policy:extended">Extended Grace Period</a></li>
<li><a href="/admin/member/{{member.username}}/policy:potato">Potato</a></li>
<li><a href="/admin/member/{{member.username}}/policy:disabled">Disabled</a></li>
</ul>
</div>

View File

@ -17,7 +17,7 @@
</tr>
{% for member in active_members %}
<tr>
<td>{{member['ldap_username']}}</td>
<td>{{member['username']}}</td>
<td>{{member['type']}}</td>
<td>{{member['joined'][0]}}/{{"%02i" |format(member['joined'][1])}}</td>
</tr>

View File

@ -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. <pre>%s</pre>" % traceback.format_exception(exc_type, exc_value,exc_traceback))
return redirect(url_for("admin_fetch"))
logic.update_transfer_rows()