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