From 90913400364fbf92a16e7e78ac4aa76b313be4b2 Mon Sep 17 00:00:00 2001
From: nd <git@notandy.de>
Date: Sun, 12 Jul 2020 23:16:28 +0200
Subject: [PATCH] added login and acls

---
 uffd/__init__.py                     |  11 ++--
 uffd/default_config.cfg              |   1 +
 uffd/group/views.py                  |   6 +++
 uffd/ldap/__init__.py                |   2 +-
 uffd/ldap/ldap.py                    |  18 ++++---
 uffd/selfservice/__init__.py         |   3 ++
 uffd/selfservice/models.py           |   0
 uffd/selfservice/templates/self.html |  74 +++++++++++++++++++++++++++
 uffd/selfservice/views.py            |  27 ++++++++++
 uffd/session/__init__.py             |   3 ++
 uffd/session/models.py               |   0
 uffd/session/templates/login.html    |  31 +++++++++++
 uffd/session/views.py                |  66 ++++++++++++++++++++++++
 uffd/static/chaosknoten.png          | Bin 0 -> 11875 bytes
 uffd/user/views.py                   |  10 +++-
 15 files changed, 239 insertions(+), 13 deletions(-)
 create mode 100644 uffd/selfservice/__init__.py
 create mode 100644 uffd/selfservice/models.py
 create mode 100644 uffd/selfservice/templates/self.html
 create mode 100644 uffd/selfservice/views.py
 create mode 100644 uffd/session/__init__.py
 create mode 100644 uffd/session/models.py
 create mode 100644 uffd/session/templates/login.html
 create mode 100644 uffd/session/views.py
 create mode 100644 uffd/static/chaosknoten.png

diff --git a/uffd/__init__.py b/uffd/__init__.py
index 8869b056..bef39eba 100644
--- a/uffd/__init__.py
+++ b/uffd/__init__.py
@@ -1,6 +1,6 @@
 import os
 
-from flask import Flask
+from flask import Flask, redirect, url_for
 from werkzeug.routing import IntegerConverter
 
 from uffd.database import db, SQLAlchemyJSON
@@ -40,12 +40,15 @@ def create_app(test_config=None):
 
 	db.init_app(app)
 	# pylint: disable=C0415
-	from uffd import user, group, csrf, ldap
+	from uffd import user, group, selfservice, session, csrf, ldap
 	# pylint: enable=C0415
 
-	for i in user.bp + group.bp + csrf.bp + ldap.bp:
+	for i in user.bp + group.bp + selfservice.bp + session.bp + csrf.bp + ldap.bp:
 		app.register_blueprint(i)
-	app.add_url_rule("/", endpoint="index")
+
+	@app.route("/")
+	def index():
+		return redirect(url_for('selfservice.self_index'))
 
 	return app
 
diff --git a/uffd/default_config.cfg b/uffd/default_config.cfg
index 229b95a6..c1c227a9 100644
--- a/uffd/default_config.cfg
+++ b/uffd/default_config.cfg
@@ -7,3 +7,4 @@ LDAP_USER_OBJECTCLASSES=["vmailUser", "top", "inetOrgPerson", "organizationalPer
 LDAP_USER_GID=20001
 LDAP_USER_MIN_UID=10000
 LDAP_USER_MAX_UID=18999
+SESSION_LIFETIME_SECONDS=3600
diff --git a/uffd/group/views.py b/uffd/group/views.py
index 1c6bcf2e..07f4ee00 100644
--- a/uffd/group/views.py
+++ b/uffd/group/views.py
@@ -2,11 +2,17 @@ from flask import Blueprint, current_app, render_template
 
 from uffd.navbar import register_navbar
 from uffd.ldap import get_conn, escape_filter_chars
+from uffd.session import login_required
 
 from .models import Group
 
 bp = Blueprint("group", __name__, template_folder='templates', url_prefix='/group/')
 
+@bp.before_request
+@login_required
+def group_acl():
+	pass
+
 @bp.route("/")
 @register_navbar('Groups', icon='layer-group', blueprint=bp)
 def group_list():
diff --git a/uffd/ldap/__init__.py b/uffd/ldap/__init__.py
index b57cd784..b420a489 100644
--- a/uffd/ldap/__init__.py
+++ b/uffd/ldap/__init__.py
@@ -1,4 +1,4 @@
 from .ldap import bp as ldap_bp
-from .ldap import get_conn, escape_filter_chars, uid_to_dn, loginname_to_dn, get_next_uid
+from .ldap import get_conn, user_conn, escape_filter_chars, uid_to_dn, loginname_to_dn, get_next_uid
 
 bp = [ldap_bp]
diff --git a/uffd/ldap/ldap.py b/uffd/ldap/ldap.py
index 6cab6495..341e410c 100644
--- a/uffd/ldap/ldap.py
+++ b/uffd/ldap/ldap.py
@@ -1,7 +1,9 @@
 from flask import Blueprint, request, session, current_app
+from ldap3.utils.conv import escape_filter_chars
+from ldap3.utils.dn import escape_rdn
+from ldap3.core.exceptions import LDAPBindError
 
 from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES
-from ldap3.utils.conv import escape_filter_chars
 
 bp = Blueprint("ldap", __name__)
 
@@ -17,15 +19,19 @@ def service_conn():
 	server = Server(current_app.config["LDAP_SERVICE_URL"], get_info=ALL)
 	return Connection(server, current_app.config["LDAP_SERVICE_BIND_DN"], current_app.config["LDAP_SERVICE_BIND_PASSWORD"], auto_bind=True)
 
-def user_conn():
-	pass
+def user_conn(loginname, password):
+	server = Server(current_app.config["LDAP_SERVICE_URL"], get_info=ALL)
+	try:
+		return fix_connection(Connection(server, loginname_to_dn(loginname), password, auto_bind=True))
+	except LDAPBindError:
+		return False
 
 def get_conn():
 	conn = service_conn()
 	return fix_connection(conn)
 
 def uid_to_dn(uid):
-	conn = service_conn()
+	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format(escape_filter_chars(uid)))
 	if not len(conn.entries) == 1:
 		return None
@@ -33,10 +39,10 @@ def uid_to_dn(uid):
 		return conn.entries[0].entry_dn
 
 def loginname_to_dn(loginname):
-	return 'uid={},{}'.format(escape_filter_chars(loginname), current_app.config["LDAP_BASE_USER"])
+	return 'uid={},{}'.format(escape_rdn(loginname), current_app.config["LDAP_BASE_USER"])
 
 def get_next_uid():
-	conn = service_conn()
+	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(objectclass=person)')
 	max_uid = current_app.config["LDAP_USER_MIN_UID"]
 	for i in conn.entries:
diff --git a/uffd/selfservice/__init__.py b/uffd/selfservice/__init__.py
new file mode 100644
index 00000000..67157866
--- /dev/null
+++ b/uffd/selfservice/__init__.py
@@ -0,0 +1,3 @@
+from .views import bp as bp_ui
+
+bp = [bp_ui]
diff --git a/uffd/selfservice/models.py b/uffd/selfservice/models.py
new file mode 100644
index 00000000..e69de29b
diff --git a/uffd/selfservice/templates/self.html b/uffd/selfservice/templates/self.html
new file mode 100644
index 00000000..6e89f8de
--- /dev/null
+++ b/uffd/selfservice/templates/self.html
@@ -0,0 +1,74 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for(".self_update") }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '')">
+<div class="align-self-center row">
+	<div class="form-group col-md-6">
+		<label for="user-uid">uid</label>
+		<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid }}" readonly>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-loginname">login name</label>
+		<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname }}" readonly>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-displayname">display name</label>
+		<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-mail">mail</label>
+		<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail }}">
+		<small class="form-text text-muted">
+		</small>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-password1">password</label>
+		<input type="password" class="form-control" id="user-password1" name="password1" placeholder="do not change">
+		<small class="form-text text-muted">
+			No special requirements but please don't be stupid and use a password manager.
+		</small>
+	</div>
+	<div class="form-group col-md-6">
+		<label for="user-password2">password repeat</label>
+		<input type="password" class="form-control" id="user-password2" name="password2" placeholder="do not change">
+	</div>
+	<div class="form-group col-md-12">
+		<button type="submit" class="btn btn-primary float-right"><i class="fa fa-save" aria-hidden="true"></i> Save</button>
+	</div>
+	<div class="form-group col-md-12" "id="accordion">
+		<div class="card">
+			<div class="card-header" id="user-group">
+				<h5 class="mb-0">
+					<a class="btn btn-link collapsed" data-toggle="collapse" data-target="#user-group-body" aria-expanded="false" aria-controls="user-group">
+						groups
+					</a>
+				</h5>
+			</div>
+			<div id="user-group-body" class="collapse" aria-labelledby="user-group" data-parent="#accordion">
+				<div class="card-body">
+					<ul class="list-group">
+					{% for group in user.get_groups() %}
+					<li class="list-group-item"><a href="{{ url_for("group.group_show", gid=group.gid) }}">{{ group.name }}</a></li>
+					{% endfor %}
+					</ul>
+				</div>
+			</div>
+		</div>
+		<div class="card">
+			<div class="card-header" id="user-role">
+				<h5 class="mb-0">
+					<a class="btn btn-link" data-toggle="collapse" data-target="#user-role-body" aria-expanded="true" aria-controls="user-role">
+						roles
+					</a>
+				</h5>
+			</div>
+			<div id="user-role-body" class="collapse show" aria-labelledby="user-role" data-parent="#accordion">
+				<div class="card-body">
+					roles.
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+</form>
+{% endblock %}
diff --git a/uffd/selfservice/views.py b/uffd/selfservice/views.py
new file mode 100644
index 00000000..a6204a56
--- /dev/null
+++ b/uffd/selfservice/views.py
@@ -0,0 +1,27 @@
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app
+
+from uffd.navbar import register_navbar
+from uffd.csrf import csrf_protect
+
+from uffd.user.models import User
+from uffd.group.models import Group
+from uffd.session import get_current_user, login_required
+from uffd.ldap import get_conn, escape_filter_chars
+
+bp = Blueprint("selfservice", __name__, template_folder='templates', url_prefix='/self/')
+
+@bp.before_request
+@login_required
+def self_acl():
+	pass
+
+@bp.route("/")
+@register_navbar('Selfservice', icon='portrait', blueprint=bp)
+def self_index():
+	return render_template('self.html', user=get_current_user())
+
+@bp.route("/update", methods=(['POST']))
+@csrf_protect
+def self_update():
+	pass
+
diff --git a/uffd/session/__init__.py b/uffd/session/__init__.py
new file mode 100644
index 00000000..2009dfac
--- /dev/null
+++ b/uffd/session/__init__.py
@@ -0,0 +1,3 @@
+from .views import bp as bp_ui, get_current_user, login_required, is_user_in_group
+
+bp = [bp_ui]
diff --git a/uffd/session/models.py b/uffd/session/models.py
new file mode 100644
index 00000000..e69de29b
diff --git a/uffd/session/templates/login.html b/uffd/session/templates/login.html
new file mode 100644
index 00000000..3c36bc30
--- /dev/null
+++ b/uffd/session/templates/login.html
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+
+{% block body %}
+<form action="{{ url_for(".login") }}" method="POST">
+<div class="row mt-2 justify-content-center">
+	<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;">
+		<div class="text-center">
+			<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" >
+		</div>
+		<div class="col-12">
+			<h2 class="text-center">Login</h2>
+		</div>
+		<div class="form-group col-12">
+			<label for="user-loginname">login name</label>
+			<input type="text" class="form-control" id="user-loginname" name="loginname" required="required" tabindex = "1">
+		</div>
+		<div class="form-group col-12">
+			<label for="user-password1">password</label>
+			<input type="password" class="form-control" id="user-password1" name="password" required="required" tabindex = "2">
+		</div>
+		<div class="form-group col-12">
+			<button type="submit" class="btn btn-primary btn-block" tabindex = "3">Login</button>
+		</div>
+		<div class="clearfix col-12">
+			<a href="#" class="float-left">Register</a>
+			<a href="#" class="float-right">Forgot Password?</a>
+		</div>
+	</div>
+</div>
+</form>
+{% endblock %}
diff --git a/uffd/session/views.py b/uffd/session/views.py
new file mode 100644
index 00000000..d1364e40
--- /dev/null
+++ b/uffd/session/views.py
@@ -0,0 +1,66 @@
+import datetime
+import functools
+
+from flask import Blueprint, render_template, request, url_for, redirect, flash, current_app, session
+
+from uffd.navbar import register_navbar
+from uffd.csrf import csrf_protect
+from uffd.user.models import User
+from uffd.ldap import get_conn, user_conn, uid_to_dn
+
+bp = Blueprint("session", __name__, template_folder='templates', url_prefix='/')
+
+@register_navbar('Logout', icon='sign-out-alt', blueprint=bp)
+@bp.route("/logout")
+def logout():
+	session.clear()
+	return redirect(url_for('.login'))
+
+@bp.route("/login", methods=('GET', 'POST'))
+def login():
+	if request.method == 'GET':
+		return render_template('login.html')
+
+	username = request.form['loginname']
+	password = request.form['password']
+	conn = user_conn(username, password)
+	if not conn:
+		flash('Login name or password is wrong')
+		return redirect(url_for('.login'))
+	conn.search(conn.user, '(objectClass=person)')
+	if not len(conn.entries) == 1:
+		flash('Login name or password is wrong')
+		return redirect(url_for('.login'))
+	user = User.from_ldap(conn.entries[0])
+	session['user_uid'] = user.uid
+	session['logintime'] = datetime.datetime.now().timestamp()
+	return redirect(url_for('index'))
+
+def get_current_user():
+	if not session.get('user_uid'):
+		return None
+	return User.from_ldap_dn(uid_to_dn(session['user_uid']))
+
+def is_valid_session():
+	user = get_current_user()
+	if not user:
+		return False
+	if datetime.datetime.now().timestamp() > session['logintime'] + current_app.config['SESSION_LIFETIME_SECONDS']:
+		flash('Session timed out')
+		return False
+	return True
+
+def is_user_in_group(user, group):
+	return True
+
+def login_required(view, group=None):
+	@functools.wraps(view)
+	def wrapped_view(**kwargs):
+		if not is_valid_session():
+			flash('You need to login first')
+			return redirect(url_for('session.login'))
+		if not is_user_in_group(get_current_user, group):
+			flash('Access denied')
+			return redirect(url_for('index'))
+		return view(**kwargs)
+	return wrapped_view
diff --git a/uffd/static/chaosknoten.png b/uffd/static/chaosknoten.png
new file mode 100644
index 0000000000000000000000000000000000000000..2a6a743bf3b454e8250fd54fd1f4c0fe444ed8ba
GIT binary patch
literal 11875
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
aL4@(<z01a3KN%Pp7(8A5T-G@yGywqpTM5ws

literal 0
HcmV?d00001

diff --git a/uffd/user/views.py b/uffd/user/views.py
index 31ef6989..ca0a7823 100644
--- a/uffd/user/views.py
+++ b/uffd/user/views.py
@@ -2,12 +2,18 @@ from flask import Blueprint, render_template, request, url_for, redirect, flash,
 
 from uffd.navbar import register_navbar
 from uffd.csrf import csrf_protect
+from uffd.ldap import get_conn, escape_filter_chars
+from uffd.session import login_required
 
 from .models import User
-from uffd.ldap import get_conn, escape_filter_chars
 
 bp = Blueprint("user", __name__, template_folder='templates', url_prefix='/user/')
 
+@bp.before_request
+@login_required
+def user_acl():
+	pass
+
 @bp.route("/")
 @register_navbar('Users', icon='users', blueprint=bp)
 def user_list():
@@ -59,8 +65,8 @@ def user_update(uid=False):
 		flash('Error updating user: {}'.format(conn.result['message']))
 	return redirect(url_for('.user_list'))
 
-@csrf_protect
 @bp.route("/<int:uid>/del")
+@csrf_protect
 def user_delete(uid):
 	conn = get_conn()
 	conn.search(current_app.config["LDAP_BASE_USER"], '(&(objectclass=person)(uidNumber={}))'.format((escape_filter_chars(uid))))
-- 
GitLab