diff --git a/uffd/__init__.py b/uffd/__init__.py index 8869b0562ae846a9f95d33419c5038caabcb9092..bef39eba8c45091e02a8ed8a29651ba0264938aa 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 229b95a62fbb218285c7d02a2410d11dd62b6387..c1c227a984324040e43915bb959b6c10e20f2ef5 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 1c6bcf2ef60b53dd6d3f967d992007a10de9bddb..07f4ee00d92208a5f99cdc976e469c8b85d43550 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 b57cd78472b79aea9bd586629d81f1e105e199de..b420a489f3ff84a9c0484b6854dea620a5e231a8 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 6cab6495dedc2dec384fe998a978a50355d83181..341e410ccb2d635893c65d498c39755b05a092f6 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 0000000000000000000000000000000000000000..671578662f91c82cb987ffe679c1f102dc493d1f --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/uffd/selfservice/templates/self.html b/uffd/selfservice/templates/self.html new file mode 100644 index 0000000000000000000000000000000000000000..6e89f8de01347f3d2c928520df69f877c49795b8 --- /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 0000000000000000000000000000000000000000..a6204a5695b9cbb615ef6a468290cf01b38a744c --- /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 0000000000000000000000000000000000000000..2009dfac27a084a878256dd63ac19ac73603f832 --- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/uffd/session/templates/login.html b/uffd/session/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..3c36bc305586d73ba95d30aa67718776f5c401d3 --- /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 0000000000000000000000000000000000000000..d1364e40a8b243ae60337856c47e2e2318331edf --- /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 Binary files /dev/null and b/uffd/static/chaosknoten.png differ diff --git a/uffd/user/views.py b/uffd/user/views.py index 31ef6989557baab6d319adb83e3df828650402d9..ca0a78238f333261e2866eb11637e18375ab5851 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))))