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 %}
+
+Username |
+E-Mail |
+Aktiv |
+Aktionen |
+{% for data in admin_data %}
+
+ {% for field in ['username', 'email', 'active'] %}
+ {{ data[field] if data[field] }} |
+ {% endfor %}
+
+
+
+ |
+
+{% endfor %}
+
+
+
+ Neuen Admin erstellen:
+
+
+
+{% 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:
+
+
+{% 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