added support for different roles (admins + super admins), super admins can create new admins
This commit is contained in:
parent
4197446a00
commit
e707f4bd87
50
imaginaerraum_door_admin/templates/admins.html
Normal file
50
imaginaerraum_door_admin/templates/admins.html
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block header %}
|
||||||
|
{% block title %}<h1>Admin Übersicht</h1>{% endblock %}
|
||||||
|
|
||||||
|
<script src="../static/jquery-3.6.0.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<table border="1">
|
||||||
|
<td>Username</td>
|
||||||
|
<td>E-Mail</td>
|
||||||
|
<td>Aktiv</td>
|
||||||
|
<td>Aktionen</td>
|
||||||
|
{% for data in admin_data %}
|
||||||
|
<tr>
|
||||||
|
{% for field in ['username', 'email', 'active'] %}
|
||||||
|
<td>{{ data[field] if data[field] }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for('admin_toggle_active', username=data['username']) }}"><img src="static/stop.png" title="Aktivieren/Deaktivieren" alt="Toggle active"></a>
|
||||||
|
<a href="{{ url_for('delete_admins', username=data['username']) }}"><img src="static/delete.png" title="Löschen" alt="Delete"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
Neuen Admin erstellen:
|
||||||
|
</p>
|
||||||
|
<form method="POST">
|
||||||
|
<table>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.name.label }}</td>
|
||||||
|
<td>{{ form.name(size=20) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.email.label }}</td>
|
||||||
|
<td>{{ form.email(size=20) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<input type="submit" value="Abschicken">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -8,6 +8,9 @@
|
||||||
<li><a href="{{ url_for('list_tokens') }}">Token Übersicht</a>
|
<li><a href="{{ url_for('list_tokens') }}">Token Übersicht</a>
|
||||||
<li><a href="{{ url_for('open_door') }}">Tür öffnen</a>
|
<li><a href="{{ url_for('open_door') }}">Tür öffnen</a>
|
||||||
<li><a href="{{ url_for('close_door') }}">Tür schließen</a>
|
<li><a href="{{ url_for('close_door') }}">Tür schließen</a>
|
||||||
|
{% if current_user.has_role('super_admin') %}
|
||||||
|
<li><a href="{{ url_for('manage_admins') }}">Admins verwalten</a>
|
||||||
|
{% endif %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
<li><a href="{{ url_for('security.change_password') }}">Passwort ändern</a>
|
<li><a href="{{ url_for('security.change_password') }}">Passwort ändern</a>
|
||||||
<li><a href="{{ url_for('security.logout') }}">Benutzer <span>{{ current_user.username }}</span> ausloggen</a>
|
<li><a href="{{ url_for('security.logout') }}">Benutzer <span>{{ current_user.username }}</span> ausloggen</a>
|
||||||
|
|
27
imaginaerraum_door_admin/templates/delete_admin.html
Normal file
27
imaginaerraum_door_admin/templates/delete_admin.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block header %}
|
||||||
|
{% block title %}<h1>Admin Benutzer löschen</h1>{% endblock %}
|
||||||
|
<script src="../static/jquery-3.6.0.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div>
|
||||||
|
Achtung, Admin '{{ username }}' wird gelöscht.
|
||||||
|
Bitte zur Bestätigung den Nutzernamen eingeben:
|
||||||
|
<form method="POST">
|
||||||
|
<table>
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ form.name.label }}</td>
|
||||||
|
<td>{{ form.name(size=20) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<input type="submit" value="Bestätigen">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -5,12 +5,14 @@ from wtforms.fields.html5 import DateField, EmailField
|
||||||
from wtforms.fields import StringField, BooleanField
|
from wtforms.fields import StringField, BooleanField
|
||||||
from wtforms.validators import DataRequired, ValidationError, EqualTo
|
from wtforms.validators import DataRequired, ValidationError, EqualTo
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
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.models import fsqla_v2 as fsqla
|
||||||
from flask_security.forms import LoginForm, Required, PasswordField
|
from flask_security.forms import LoginForm, Required, PasswordField
|
||||||
from flask_security.utils import find_user, verify_password
|
from flask_security.utils import find_user, verify_password
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from email_validator import validate_email
|
from email_validator import validate_email
|
||||||
|
|
||||||
|
import secrets
|
||||||
import bleach
|
import bleach
|
||||||
import ldap3
|
import ldap3
|
||||||
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
|
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 = StringField('Name', validators=[DataRequired(), EqualTo('name_confirm', 'Name stimmt nicht überein')])
|
||||||
name_confirm = StringField('Name confirm')
|
name_confirm = StringField('Name confirm')
|
||||||
|
|
||||||
|
class AdminCreationForm(FlaskForm):
|
||||||
|
name = StringField('Name', validators=[DataRequired()])
|
||||||
|
email = EmailField('E-Mail', validators=[DataRequired()])
|
||||||
|
|
||||||
def uia_username_mapper(identity):
|
def uia_username_mapper(identity):
|
||||||
# we allow pretty much anything - but we bleach it.
|
# we allow pretty much anything - but we bleach it.
|
||||||
return bleach.clean(identity, strip=True)
|
return bleach.clean(identity, strip=True)
|
||||||
|
@ -76,22 +82,6 @@ def create_application(config):
|
||||||
if not Path(config.token_file).exists():
|
if not Path(config.token_file).exists():
|
||||||
logger.warning(f"Token file not found at {config.token_file}")
|
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"'<username> <email> <password>\\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
|
# 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,
|
door = DoorHandle(token_file=config.token_file, mqtt_host=config.mqtt_host, nfc_socket=config.nfc_socket,
|
||||||
logger=logger)
|
logger=logger)
|
||||||
|
@ -210,22 +200,74 @@ def create_application(config):
|
||||||
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
|
||||||
security = Security(app, user_datastore, login_form=ExtendedLoginForm)
|
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
|
@app.route('/manage_admins', methods=['GET', 'POST'])
|
||||||
def create_user():
|
@roles_required('super_admin')
|
||||||
db.create_all()
|
def manage_admins():
|
||||||
if len(new_admin_data) > 0:
|
form = AdminCreationForm()
|
||||||
# create admin accounts from given file
|
if request.method == 'GET':
|
||||||
create_admins(new_admin_data)
|
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()
|
db.session.commit()
|
||||||
|
return redirect('/manage_admins')
|
||||||
|
|
||||||
|
@app.route('/delete_admins/<username>', 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/<username>')
|
||||||
|
@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('/')
|
@app.route('/')
|
||||||
def door_lock():
|
def door_lock():
|
||||||
|
@ -269,7 +311,7 @@ def create_application(config):
|
||||||
else:
|
else:
|
||||||
session['valid_thru'] = ''
|
session['valid_thru'] = ''
|
||||||
session['inactive'] = not form.active.data
|
session['inactive'] = not form.active.data
|
||||||
return redirect(f'/store-token')
|
return redirect('/store-token')
|
||||||
else:
|
else:
|
||||||
return render_template('register.html', token=door.get_most_recent_token(), form=form)
|
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}')
|
flash(f'Could not close door. Exception: {e}')
|
||||||
return redirect('/')
|
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"'<username> <email> <password>\\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
|
return app
|
||||||
|
|
Loading…
Reference in New Issue
Block a user