improved admin user creation and added basic logging features
This commit is contained in:
parent
eb4c027f46
commit
4197446a00
|
@ -9,6 +9,7 @@ parser.add_argument("--nfc_socket", default="/tmp/nfc.sock", help="socket for ha
|
||||||
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("--log_file", default="/var/log/webinterface.log", help="Path to log file")
|
||||||
parser.add_argument("--ldap_url", default="ldaps://do.imaginaerraum.de",
|
parser.add_argument("--ldap_url", default="ldaps://do.imaginaerraum.de",
|
||||||
help="URL for LDAP server for alternative user authorization")
|
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")
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
import socket
|
import socket
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
|
||||||
class DoorHandle:
|
class DoorHandle:
|
||||||
def __init__(self, token_file, mqtt_host, mqtt_port=1883, nfc_socket='/tmp/nfc.sock'):
|
def __init__(self, token_file, mqtt_host, mqtt_port=1883, nfc_socket='/tmp/nfc.sock', logger=None):
|
||||||
self.state = None
|
self.state = None
|
||||||
self.encoder_position = None
|
self.encoder_position = None
|
||||||
|
|
||||||
|
@ -20,18 +20,25 @@ class DoorHandle:
|
||||||
self.mqtt_client.connect_async(host=mqtt_host, port=mqtt_port)
|
self.mqtt_client.connect_async(host=mqtt_host, port=mqtt_port)
|
||||||
self.mqtt_client.loop_start()
|
self.mqtt_client.loop_start()
|
||||||
|
|
||||||
|
if logger:
|
||||||
|
self.logger = logger
|
||||||
|
else:
|
||||||
|
self.logger = logging
|
||||||
|
|
||||||
self.nfc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
self.nfc_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
try:
|
try:
|
||||||
self.nfc_sock.connect(nfc_socket)
|
self.nfc_sock.connect(nfc_socket)
|
||||||
|
self.logger.info(f"Connected to NFC socket at {nfc_socket}.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Could not connect to NFC socket at {nfc_socket}. Exception: {e}")
|
print(f"Could not connect to NFC socket at {nfc_socket}. Exception: {e}")
|
||||||
|
self.nfc_sock = None
|
||||||
#raise
|
#raise
|
||||||
|
|
||||||
self.data_fields = ['name', 'organization', 'email', 'valid_thru']
|
self.data_fields = ['name', 'organization', 'email', 'valid_thru']
|
||||||
|
|
||||||
# The callback for when the client receives a CONNACK response from the server.
|
# The callback for when the client receives a CONNACK response from the server.
|
||||||
def on_connect(self, client, userdata, flags, rc):
|
def on_connect(self, client, userdata, flags, rc):
|
||||||
print("Connected with result code " + str(rc))
|
self.logger.info("Connected to MQTT broker with result code " + str(rc))
|
||||||
|
|
||||||
# Subscribing in on_connect() means that if we lose the connection and
|
# Subscribing in on_connect() means that if we lose the connection and
|
||||||
# reconnect then subscriptions will be renewed.
|
# reconnect then subscriptions will be renewed.
|
||||||
|
@ -81,13 +88,21 @@ class DoorHandle:
|
||||||
# write new tokens to file and trigger reload
|
# write new tokens to file and trigger reload
|
||||||
with open(self.token_file, 'w') as f:
|
with open(self.token_file, 'w') as f:
|
||||||
f.write(output)
|
f.write(output)
|
||||||
|
|
||||||
|
if self.nfc_sock is not None:
|
||||||
self.nfc_sock.send(b'rld\n')
|
self.nfc_sock.send(b'rld\n')
|
||||||
|
|
||||||
def open_door(self):
|
def open_door(self):
|
||||||
|
if self.nfc_sock is not None:
|
||||||
self.nfc_sock.send(b'open\n')
|
self.nfc_sock.send(b'open\n')
|
||||||
|
else:
|
||||||
|
raise Exception("No connection to NFC socket. Cannot close door!")
|
||||||
|
|
||||||
def close_door(self):
|
def close_door(self):
|
||||||
|
if self.nfc_sock is not None:
|
||||||
self.nfc_sock.send(b'close\n')
|
self.nfc_sock.send(b'close\n')
|
||||||
|
else:
|
||||||
|
raise Exception("No connection to NFC socket. Cannot close door!")
|
||||||
|
|
||||||
def get_most_recent_token(self):
|
def get_most_recent_token(self):
|
||||||
# read last invalid token from logfile
|
# read last invalid token from logfile
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<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>
|
||||||
<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 %}
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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
|
from flask_security import Security, SQLAlchemyUserDatastore, auth_required, hash_password, uia_email_mapper, current_user
|
||||||
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
|
||||||
|
@ -14,6 +14,8 @@ from email_validator import validate_email
|
||||||
import bleach
|
import bleach
|
||||||
import ldap3
|
import ldap3
|
||||||
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
|
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
|
||||||
|
from pathlib import Path
|
||||||
|
import logging
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from .door_handle import DoorHandle
|
from .door_handle import DoorHandle
|
||||||
|
@ -48,8 +50,51 @@ def uia_username_mapper(identity):
|
||||||
return bleach.clean(identity, strip=True)
|
return bleach.clean(identity, strip=True)
|
||||||
|
|
||||||
def create_application(config):
|
def create_application(config):
|
||||||
|
# set up logging for the web app
|
||||||
|
logger = logging.getLogger('webapp')
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
if config.log_file is not None:
|
||||||
|
ch = logging.FileHandler(config.log_file)
|
||||||
|
ch.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
# create console handler and set level to debug
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(logging.DEBUG)
|
||||||
|
# create formatter
|
||||||
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
# add formatter to ch
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
# add ch to logger
|
||||||
|
logger.addHandler(ch)
|
||||||
|
|
||||||
|
# do some checks for file existence etc.
|
||||||
|
if not Path(config.template_folder).exists():
|
||||||
|
logger.error(f'Flask template folder not found at {config.template_folder}')
|
||||||
|
if not Path(config.static_folder).exists():
|
||||||
|
logger.error(f'Flask static folder not found at {config.static_folder}')
|
||||||
|
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"'<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)
|
||||||
|
|
||||||
app = Flask(__name__, template_folder=config.template_folder, static_folder=config.static_folder)
|
app = Flask(__name__, template_folder=config.template_folder, static_folder=config.static_folder)
|
||||||
|
|
||||||
|
@ -153,6 +198,10 @@ def create_application(config):
|
||||||
# authorization in LDAP uses username -> get username associated with email from the database
|
# authorization in LDAP uses username -> get username associated with email from the database
|
||||||
user = find_user(self.email.data)
|
user = find_user(self.email.data)
|
||||||
authorized = validate_ldap(user, self.password.data)
|
authorized = validate_ldap(user, self.password.data)
|
||||||
|
if authorized:
|
||||||
|
logger.info(f"Admin user with credentials '{self.email.data}' authorized through LDAP")
|
||||||
|
else:
|
||||||
|
logger.info(f"Admin user with credentials '{self.email.data}' authorized through local database")
|
||||||
|
|
||||||
# if any of the authorization methods is successful we authorize the user
|
# if any of the authorization methods is successful we authorize the user
|
||||||
return authorized
|
return authorized
|
||||||
|
@ -162,28 +211,20 @@ def create_application(config):
|
||||||
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)
|
# create admin users (only if they don't exists already)
|
||||||
def create_admins(admin_user_file):
|
def create_admins(new_admin_data):
|
||||||
with open(admin_user_file) as f:
|
for d in new_admin_data:
|
||||||
admin_data = f.readlines()
|
if user_datastore.find_user(email=d['email'], username=d['username']) is None:
|
||||||
for i, d in enumerate(admin_data):
|
logger.info(f"New admin user created with username '{d['username']}' and email '{d['email']}'")
|
||||||
try:
|
|
||||||
user, email, pw = d.split()
|
|
||||||
if user_datastore.find_user(email=email, username=user) is None:
|
|
||||||
validate_email(email)
|
|
||||||
# create new admin (only if admin does not already exist)
|
# create new admin (only if admin does not already exist)
|
||||||
user_datastore.create_user(email=email, username=user, password=hash_password(pw))
|
user_datastore.create_user(email=d['email'], username=d['username'], password=hash_password(d['password']))
|
||||||
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.")
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Create a user to test with
|
# Create a user to test with
|
||||||
@app.before_first_request
|
@app.before_first_request
|
||||||
def create_user():
|
def create_user():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
if config.admin_file is not None:
|
if len(new_admin_data) > 0:
|
||||||
# create admin accounts from given file
|
# create admin accounts from given file
|
||||||
create_admins(config.admin_file)
|
create_admins(new_admin_data)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
|
@ -306,6 +347,7 @@ def create_application(config):
|
||||||
'organization': session['organization']}
|
'organization': session['organization']}
|
||||||
try:
|
try:
|
||||||
door.store_tokens(tokens)
|
door.store_tokens(tokens)
|
||||||
|
logger.info(f"Token {token} stored in database by admin user {current_user.username}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Error during store_tokens. Exception: {e}")
|
flash(f"Error during store_tokens. Exception: {e}")
|
||||||
return redirect('/tokens')
|
return redirect('/tokens')
|
||||||
|
@ -338,6 +380,7 @@ def create_application(config):
|
||||||
tokens.pop(token)
|
tokens.pop(token)
|
||||||
try:
|
try:
|
||||||
door.store_tokens(tokens)
|
door.store_tokens(tokens)
|
||||||
|
logger.info(f"Token {token} was deleted from database by admin user {current_user.username}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Error during store_tokens. Exception: {e}")
|
flash(f"Error during store_tokens. Exception: {e}")
|
||||||
flash(f"Token {token} wurde gelöscht!")
|
flash(f"Token {token} wurde gelöscht!")
|
||||||
|
@ -366,6 +409,7 @@ def create_application(config):
|
||||||
tokens[token]['inactive'] = True
|
tokens[token]['inactive'] = True
|
||||||
try:
|
try:
|
||||||
door.store_tokens(tokens)
|
door.store_tokens(tokens)
|
||||||
|
logger.info(f"Token {token} deactivated by admin user {current_user.username}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Error during store_tokens. Exception: {e}")
|
flash(f"Error during store_tokens. Exception: {e}")
|
||||||
return redirect('/tokens')
|
return redirect('/tokens')
|
||||||
|
@ -375,6 +419,7 @@ def create_application(config):
|
||||||
def open_door():
|
def open_door():
|
||||||
try:
|
try:
|
||||||
door.open_door()
|
door.open_door()
|
||||||
|
logger.info(f"Door opened by admin user {current_user.username}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f'Could not open door. Exception: {e}')
|
flash(f'Could not open door. Exception: {e}')
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
@ -384,6 +429,7 @@ def create_application(config):
|
||||||
def close_door():
|
def close_door():
|
||||||
try:
|
try:
|
||||||
door.close_door()
|
door.close_door()
|
||||||
|
logger.info(f"Door closed by admin user {current_user.username}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f'Could not close door. Exception: {e}')
|
flash(f'Could not close door. Exception: {e}')
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user