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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
B3DE!m

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