Compare commits

...

2 Commits

Author SHA1 Message Date
1e87406fdb added LDAP authorization 2021-03-22 21:22:51 +01:00
e79713e094 Added confirmation page for token deletion 2021-03-21 21:45:13 +01:00
4 changed files with 106 additions and 29 deletions

View File

@ -3,13 +3,14 @@ import argparse
from imaginaerraum_door_admin.webapp import create_application from imaginaerraum_door_admin.webapp import create_application
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--token_file", default="/etc/door_tokens", help="path to the file with door tokens and users") parser.add_argument("--token_file", default="/etc/door_tokens", help="path to the file with door tokens and users")
parser.add_argument("--nfc_socket", default="/tmp/nfc.sock", help="socket for handling NFC reader commands") parser.add_argument("--nfc_socket", default="/tmp/nfc.sock", help="socket for handling NFC reader commands")
parser.add_argument("--template_folder", default="templates", help="path to Flask templates folder") parser.add_argument("--template_folder", default="templates", help="path to Flask templates folder")
parser.add_argument("--static_folder", default="static", help="path to Flask static folder") parser.add_argument("--static_folder", default="static", help="path to Flask static folder")
parser.add_argument("--admin_file", help="Path to file for creating initial admin users") parser.add_argument("--admin_file", help="Path to file for creating initial admin users")
parser.add_argument("--ldap_url", default="ldaps://do.imaginaerraum.de",
help="URL for LDAP server for alternative user authorization")
parser.add_argument("--mqtt_host", default="10.10.21.2", help="IP address of MQTT broker") parser.add_argument("--mqtt_host", default="10.10.21.2", help="IP address of MQTT broker")
parser.add_argument("--port", default=80, help="Port for running the Flask server") parser.add_argument("--port", default=80, help="Port for running the Flask server")
parser.add_argument("--mail_server", default="smtp.googlemail.com", help="email server for sending security messages") parser.add_argument("--mail_server", default="smtp.googlemail.com", help="email server for sending security messages")

View File

@ -0,0 +1,27 @@
{% extends 'base.html' %}
{% block header %}
{% block title %}<h1>Token löschen</h1>{% endblock %}
<script src="../static/jquery-3.6.0.js"></script>
{% endblock %}
{% block content %}
<div>
Achtung, der Token von NutzerIn '{{ token['name'] }}' 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 %}

View File

@ -22,7 +22,7 @@
<td> <td>
<a href="{{ url_for('edit_token', token=t) }}"><img src="static/edit.png" title="Editieren" alt="Edit"></a> <a href="{{ url_for('edit_token', token=t) }}"><img src="static/edit.png" title="Editieren" alt="Edit"></a>
<a href="{{ url_for('deactivate_token', token=t) }}"><img src="static/stop.png" title="Deaktivieren" alt="Deactivate"></a> <a href="{{ url_for('deactivate_token', token=t) }}"><img src="static/stop.png" title="Deaktivieren" alt="Deactivate"></a>
<img src="static/delete.png" title="Löschen" alt="Delete" onclick="confirmDelete('{{ t }}')"> <a href="{{ url_for('delete_token', token=t) }}"><img src="static/delete.png" title="Löschen" alt="Delete"></a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -39,20 +39,4 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
<script>
function confirmDelete(t) {
debugger
if (confirm('Token wirklich löschen?')) {
console.log('confirmed');
console.log(t);
$.post('{{ url_for('delete_token') }}', {token: t},
function (data) {
if (data === 'success') {
location.reload();
}
});
}
}
</script>
{% endblock %} {% endblock %}

View File

@ -1,16 +1,18 @@
import os import os
from flask import Flask, render_template, request, flash, redirect, session from flask import Flask, render_template, request, flash, redirect, session, url_for
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms.fields.html5 import DateField, EmailField 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 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 from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password, uia_email_mapper
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_mail import Mail from flask_mail import Mail
from flask_security.utils import find_user
from email_validator import validate_email from email_validator import validate_email
import bleach import bleach
import ldap3
from datetime import date from datetime import date
from .door_handle import DoorHandle from .door_handle import DoorHandle
@ -36,6 +38,9 @@ class TokenForm(FlaskForm):
active = BooleanField('Aktiv?') active = BooleanField('Aktiv?')
dsgvo = BooleanField('Einwilligung Nutzungsbedingungen erfragt?', validators=[DataRequired()]) dsgvo = BooleanField('Einwilligung Nutzungsbedingungen erfragt?', validators=[DataRequired()])
class TokenDeleteForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), EqualTo('name_confirm', 'Name stimmt nicht überein')])
name_confirm = StringField('Name confirm')
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.
@ -93,10 +98,42 @@ def create_application(config):
class User(db.Model, fsqla.FsUserMixin): class User(db.Model, fsqla.FsUserMixin):
username = db.Column(db.String(255)) username = db.Column(db.String(255))
# LDAP
ldap_server = ldap3.Server(config.ldap_url)
def validate_ldap(user, password):
# try to connect to the LDAP server
# if the connection completes successfully the given user and password is authorized
try:
con = ldap3.Connection(ldap_server, user="uid=%s,ou=Users,dc=imaginaerraum,dc=de" % (user.username,),
password=password, auto_bind=True)
except Exception:
return False
return con is not None
class ExtendedLoginForm(LoginForm): class ExtendedLoginForm(LoginForm):
email = StringField('Benutzername oder E-Mail', [Required()]) email = StringField('Benutzername oder E-Mail', [Required()])
password = PasswordField('Passwort', [Required()]) password = PasswordField('Passwort', [Required()])
def validate(self):
# authorization in LDAP uses username -> get username associated with email from the database
user = find_user(self.email.data)
# try authorizing using LDAP
response_ldap = validate_ldap(user, self.password.data)
if response_ldap:
# if LDAP authorization succeeds we update the currently stored password in the Flask user datastore
# with the one used for LDAP authorization. This way we can authorize with the LDAP password later
# even if the server is not reachable
user.password = hash_password(self.password.data)
# try authorizing using Flask security
response_orig = super(ExtendedLoginForm, self).validate()
# if any of the authorization methods is successful we authorize the user
return response_ldap or response_orig
app.config['SECURITY_MSG_USERID_NOT_PROVIDED'] = ('User ID not provided', 'error')
# Setup Flask-Security # Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role) user_datastore = SQLAlchemyUserDatastore(db, User, Role)
@ -245,13 +282,16 @@ def create_application(config):
'valid_thru': session['valid_thru'], 'valid_thru': session['valid_thru'],
'inactive': session['inactive'], 'inactive': session['inactive'],
'organization': session['organization']} 'organization': session['organization']}
door.store_tokens(tokens) try:
door.store_tokens(tokens)
except Exception as e:
flash(f"Error during store_tokens. Exception: {e}")
return redirect('/tokens') return redirect('/tokens')
@app.route('/delete-token', methods=['POST']) @app.route('/delete-token/<token>', methods=['GET', 'POST'])
@auth_required() @auth_required()
def delete_token(): def delete_token(token):
"""Delete the given token from the token file and store the new token file to disk """Delete the given token from the token file and store the new token file to disk
Parameters Parameters
@ -259,12 +299,34 @@ def create_application(config):
token : str token : str
The token to delete from the database. The token to delete from the database.
""" """
token = request.form.get('token')
tokens = door.get_tokens() tokens = door.get_tokens()
if token in tokens: # check if token exists
tokens.pop(token) if token in tokens:
door.store_tokens(tokens) token_to_delete = tokens[token]
return "success"
# set up form for confirming deletion
form = TokenDeleteForm()
form.name_confirm.data = token_to_delete['name']
if request.method == 'GET':
# return page asking the user to confirm delete
return render_template('delete.html', token=token_to_delete, form=form)
elif form.validate():
# form validation successful -> can delete the token
tokens.pop(token)
try:
door.store_tokens(tokens)
except Exception as e:
flash(f"Error during store_tokens. Exception: {e}")
flash(f"Token {token} wurde gelöscht!")
return redirect('/tokens')
else:
# form validation failed -> return to token overview and flash message
flash(f"Der eingegebene Name stimmt nicht überein. Der Token {token} von {token_to_delete['name']} wurde nicht gelöscht.")
return redirect('/tokens')
else:
flash(f'Ungültiger Token {token} für Löschung.')
return redirect('/tokens')
@app.route('/deactivate-token/<token>') @app.route('/deactivate-token/<token>')
@ -280,7 +342,10 @@ def create_application(config):
tokens = door.get_tokens() tokens = door.get_tokens()
if token in tokens: if token in tokens:
tokens[token]['inactive'] = True tokens[token]['inactive'] = True
door.store_tokens(tokens) try:
door.store_tokens(tokens)
except Exception as e:
flash(f"Error during store_tokens. Exception: {e}")
return redirect('/tokens') return redirect('/tokens')
@app.route('/open') @app.route('/open')