diff --git a/imaginaerraum_door_admin/templates/admins.html b/imaginaerraum_door_admin/templates/admins.html new file mode 100644 index 0000000..9d348e0 --- /dev/null +++ b/imaginaerraum_door_admin/templates/admins.html @@ -0,0 +1,50 @@ +{% extends 'base.html' %} +{% block header %} + {% block title %}

Admin Übersicht

{% endblock %} + + +{% endblock %} + +{% block content %} + + + + + +{% for data in admin_data %} + + {% for field in ['username', 'email', 'active'] %} + + {% endfor %} + + +{% endfor %} +
UsernameE-MailAktivAktionen
{{ data[field] if data[field] }} + Toggle active + Delete +
+
+

+ Neuen Admin erstellen: +

+
+ + {{ form.csrf_token }} + + + + + + + + + + + + +
{{ form.name.label }}{{ form.name(size=20) }}
{{ form.email.label }}{{ form.email(size=20) }}
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/imaginaerraum_door_admin/templates/base.html b/imaginaerraum_door_admin/templates/base.html index 9817a93..99b2e9a 100644 --- a/imaginaerraum_door_admin/templates/base.html +++ b/imaginaerraum_door_admin/templates/base.html @@ -8,6 +8,9 @@
  • Token Übersicht
  • Tür öffnen
  • Tür schließen + {% if current_user.has_role('super_admin') %} +
  • Admins verwalten + {% endif %} {% if current_user.is_authenticated %}
  • Passwort ändern
  • Benutzer {{ current_user.username }} ausloggen diff --git a/imaginaerraum_door_admin/templates/delete_admin.html b/imaginaerraum_door_admin/templates/delete_admin.html new file mode 100644 index 0000000..27bf516 --- /dev/null +++ b/imaginaerraum_door_admin/templates/delete_admin.html @@ -0,0 +1,27 @@ +{% extends 'base.html' %} +{% block header %} + {% block title %}

    Admin Benutzer löschen

    {% endblock %} + +{% endblock %} + +{% block content %} +
    + Achtung, Admin '{{ username }}' wird gelöscht. + Bitte zur Bestätigung den Nutzernamen eingeben: +
    + + {{ form.csrf_token }} + + + + + + + + +
    {{ form.name.label }}{{ form.name(size=20) }}
    + +
    +
    +
    +{% endblock %} \ No newline at end of file diff --git a/imaginaerraum_door_admin/webapp.py b/imaginaerraum_door_admin/webapp.py index 508ad6c..a5becec 100644 --- a/imaginaerraum_door_admin/webapp.py +++ b/imaginaerraum_door_admin/webapp.py @@ -5,12 +5,14 @@ from wtforms.fields.html5 import DateField, EmailField from wtforms.fields import StringField, BooleanField from wtforms.validators import DataRequired, ValidationError, EqualTo from flask_sqlalchemy import SQLAlchemy -from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password, uia_email_mapper, current_user +from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password, uia_email_mapper, current_user, roles_required from flask_security.models import fsqla_v2 as fsqla from flask_security.forms import LoginForm, Required, PasswordField from flask_security.utils import find_user, verify_password from flask_mail import Mail from email_validator import validate_email + +import secrets import bleach import ldap3 from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError @@ -45,6 +47,10 @@ class TokenDeleteForm(FlaskForm): name = StringField('Name', validators=[DataRequired(), EqualTo('name_confirm', 'Name stimmt nicht überein')]) name_confirm = StringField('Name confirm') +class AdminCreationForm(FlaskForm): + name = StringField('Name', validators=[DataRequired()]) + email = EmailField('E-Mail', validators=[DataRequired()]) + def uia_username_mapper(identity): # we allow pretty much anything - but we bleach it. return bleach.clean(identity, strip=True) @@ -76,22 +82,6 @@ def create_application(config): if not Path(config.token_file).exists(): logger.warning(f"Token file not found at {config.token_file}") - new_admin_data = [] - if config.admin_file is not None: - if not Path(config.admin_file).exists(): - logger.warning(f"Admin user creation file not found at {config.admin_file}") - else: - # store data for new admins in memory s.t. the file can be deleted afterwards - with open(config.admin_file) as f: - for i, d in enumerate(f.readlines()): - try: - user, email, pw = d.split() - validate_email(email) - new_admin_data.append({'username': user, 'email': email, 'password': pw}) - except Exception as e: - print(f"Error while parsing line {i} in admin config file. Config file should contain lines of " - f"' \\n'\n Exception: {e}\nAdmin account could not be created.") - # create door objects which provides access to the token file and current door state via MQTT door = DoorHandle(token_file=config.token_file, mqtt_host=config.mqtt_host, nfc_socket=config.nfc_socket, logger=logger) @@ -210,22 +200,74 @@ def create_application(config): user_datastore = SQLAlchemyUserDatastore(db, User, Role) security = Security(app, user_datastore, login_form=ExtendedLoginForm) - # create admin users (only if they don't exists already) - def create_admins(new_admin_data): - for d in new_admin_data: - if user_datastore.find_user(email=d['email'], username=d['username']) is None: - logger.info(f"New admin user created with username '{d['username']}' and email '{d['email']}'") - # create new admin (only if admin does not already exist) - user_datastore.create_user(email=d['email'], username=d['username'], password=hash_password(d['password'])) - # Create a user to test with - @app.before_first_request - def create_user(): - db.create_all() - if len(new_admin_data) > 0: - # create admin accounts from given file - create_admins(new_admin_data) + + @app.route('/manage_admins', methods=['GET', 'POST']) + @roles_required('super_admin') + def manage_admins(): + form = AdminCreationForm() + if request.method == 'GET': + users = user_datastore.user_model.query.all() + admin_data = [{'username': u.username, 'email': u.email, 'active': u.is_active} for u in users] + return render_template('admins.html', admin_data=admin_data, form=form) + elif form.validate(): + if user_datastore.find_user(username=form.name.data) is not None or \ + user_datastore.find_user(email=form.email.data) is not None: + flash("A user with the same name or email is already registered!") + return redirect('/manage_admins') + else: + pw = secrets.token_urlsafe(16) + new_user = user_datastore.create_user(username=form.name.data, email=form.email.data, roles=['admin'], + password=hash_password(pw)) + logger.info(f"Super admin {current_user.username} created new admin account for {new_user.username} <{new_user.email}>") + flash(f"An account for the new admin user {new_user.username} has been created. Use the randomly generated password {pw} to log in.") + db.session.commit() + return redirect('/manage_admins') + + @app.route('/delete_admins/', methods=['GET', 'POST']) + @roles_required('super_admin') + def delete_admins(username): + user = user_datastore.find_user(username=username) + if user is None: + flash(f"Invalid user {username}") + return redirect('/manage_admins') + if user.has_role('super_admin'): + flash('Cannot delete super admins!') + return redirect('/manage_admins') + if user.is_active: + flash('Cannot delete active users. Please deactivate user first!') + return redirect('/manage_admins') + + # set up form for confirming deletion + form = TokenDeleteForm() + form.name_confirm.data = username + + if request.method == 'GET': + # return page asking the user to confirm delete + return render_template('delete_admin.html', username=username, form=form) + elif form.validate(): + user_datastore.delete_user(user) + logger.info(f"Super admin {current_user.username} deleted admin user {username}") + db.session.commit() + return redirect('/manage_admins') + + @app.route('/admin_toggle_active/') + @roles_required('super_admin') + def admin_toggle_active(username): + user = user_datastore.find_user(username=username) + if user is None: + flash(f"Invalid user {username}") + return redirect('/manage_admins') + if user.has_role('super_admin'): + flash('Cannot deactivate super admins!') + return redirect('/manage_admins') + user_datastore.toggle_active(user) + if user.is_active: + logger.info(f"Super admin {current_user.username} activated access for admin user {username}") + else: + logger.info(f"Super admin {current_user.username} deactivated access for admin user {username}") db.session.commit() + return redirect('/manage_admins') @app.route('/') def door_lock(): @@ -269,7 +311,7 @@ def create_application(config): else: session['valid_thru'] = '' session['inactive'] = not form.active.data - return redirect(f'/store-token') + return redirect('/store-token') else: return render_template('register.html', token=door.get_most_recent_token(), form=form) @@ -434,4 +476,40 @@ def create_application(config): flash(f'Could not close door. Exception: {e}') return redirect('/') + # setup user database when starting the app + with app.app_context(): + new_admin_data = [] + if config.admin_file is not None: + if not Path(config.admin_file).exists(): + logger.warning(f"Admin user creation file not found at {config.admin_file}") + else: + # store data for new admins in memory s.t. the file can be deleted afterwards + with open(config.admin_file) as f: + for i, d in enumerate(f.readlines()): + try: + user, email, pw = d.split() + validate_email(email) + new_admin_data.append({'username': user, 'email': email, 'password': pw}) + except Exception as e: + print( + f"Error while parsing line {i} in admin config file. Config file should contain lines of " + f"' \\n'\n Exception: {e}\nAdmin account could not be created.") + + # create admin users (only if they don't exists already) + def create_super_admins(new_admin_data): + db.create_all() + super_admin_role = user_datastore.find_or_create_role('super_admin') # root admin = can create other admins + admin_role = user_datastore.find_or_create_role('admin') # 'normal' admin + + for d in new_admin_data: + if user_datastore.find_user(email=d['email'], username=d['username']) is None: + logger.info(f"New admin user created with username '{d['username']}' and email '{d['email']}'") + # create new admin (only if admin does not already exist) + new_admin = user_datastore.create_user(email=d['email'], username=d['username'], + password=hash_password(d['password']), + roles=[super_admin_role, admin_role]) + db.session.commit() + + create_super_admins(new_admin_data) + return app