Compare commits
No commits in common. "d9a078a1142606fc8aea22444d179edb0b23ddd4" and "6d9e90631a6dbf323ebef8151b0d271726d29405" have entirely different histories.
d9a078a114
...
6d9e90631a
|
@ -1,4 +1,4 @@
|
||||||
Flask-based web interface for user token administration of our hackerspace's door lock.
|
Flask-based web interface for user token adminstration of our hackerspace's door lock.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Clone the repo
|
Clone the repo
|
||||||
|
@ -49,7 +49,7 @@ definitely should set custom values for the ``SECRET_KEY`` and
|
||||||
``SECURITY_PASSWORD_SALT``.
|
``SECURITY_PASSWORD_SALT``.
|
||||||
|
|
||||||
### Token file
|
### Token file
|
||||||
The token file is an ASCII file which lists the IDs of RFID tokens that can be
|
The token file is an ASCII file which list the IDs of RFID tokens that can be
|
||||||
used to unlock the door. You can specify the path to the token file using the
|
used to unlock the door. You can specify the path to the token file using the
|
||||||
``TOKEN_FILE`` variable in the configuration file.
|
``TOKEN_FILE`` variable in the configuration file.
|
||||||
Here's an example of a token file (lines starting with ``#`` represent inactive
|
Here's an example of a token file (lines starting with ``#`` represent inactive
|
||||||
|
|
|
@ -1,11 +1,28 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from imaginaerraum_door_admin import create_app
|
import argparse
|
||||||
|
|
||||||
|
from imaginaerraum_door_admin.webapp import create_application
|
||||||
|
|
||||||
def main():
|
parser = argparse.ArgumentParser()
|
||||||
app = create_app()
|
parser.add_argument("--key_file", default='/root/flask_keys', help="Path to file with Flask SECRET_KEY and SECURITY_PASSWORD_SALT")
|
||||||
app.run(host='0.0.0.0', port=app.config.get('PORT', 80))
|
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("--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("--admin_file", default="/etc/admins.conf", help="Path to file for creating super admin users")
|
||||||
|
parser.add_argument("--log_file", default="/var/log/webinterface.log", help="Path to flask log file")
|
||||||
|
parser.add_argument("--nfc_log", default="/var/log/nfc.log", help="Path to nfc log file")
|
||||||
|
parser.add_argument("--ldap_url", default="ldaps://ldap.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("--flask_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_port", default=465, help="port for security email server")
|
||||||
|
parser.add_argument("--mail_use_tls", default=False, help="use TLS for security emails")
|
||||||
|
parser.add_argument("--mail_use_ssl", default=True, help="use SSL for security emails")
|
||||||
|
parser.add_argument("--mail_username", default="admin@example.com", help="email account for sending security messages")
|
||||||
|
parser.add_argument("--mail_password", default="password", help="Password for email account")
|
||||||
|
config = parser.parse_args()
|
||||||
|
|
||||||
|
app = create_application(config)
|
||||||
if __name__ == "__main__":
|
app.run(host='0.0.0.0', port=config.flask_port)
|
||||||
main()
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ security = Security()
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
|
||||||
# create admin users (only if they don't exist already)
|
# create admin users (only if they don't exists already)
|
||||||
def create_super_admins(app, user_datastore):
|
def create_super_admins(app, user_datastore):
|
||||||
admin_file = Path(app.config.get('ADMIN_FILE'))
|
admin_file = Path(app.config.get('ADMIN_FILE'))
|
||||||
|
|
||||||
|
@ -37,10 +37,8 @@ def create_super_admins(app, user_datastore):
|
||||||
'password': pw})
|
'password': pw})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(
|
app.logger.error(
|
||||||
f"Error while parsing line {i} in admin config file. "
|
f"Error while parsing line {i} in admin config file. Config file should contain lines of "
|
||||||
f"Config file should contain lines of <username> "
|
f"'<username> <email> <password>\\n'\n Exception: {e}\nAdmin account could not be created."
|
||||||
f"<email> <password>\\n'\n "
|
|
||||||
f"Exception: {e}\nAdmin account could not be created."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
|
@ -75,11 +73,10 @@ def create_super_admins(app, user_datastore):
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(
|
app.config.from_object('imaginaerraum_door_admin.default_app_config.DefaultConfig')
|
||||||
'imaginaerraum_door_admin.default_app_config.DefaultConfig'
|
|
||||||
)
|
|
||||||
app.config.from_envvar('APPLICATION_SETTINGS', silent=True)
|
app.config.from_envvar('APPLICATION_SETTINGS', silent=True)
|
||||||
|
|
||||||
|
|
||||||
token_file = Path(app.config.get('TOKEN_FILE'))
|
token_file = Path(app.config.get('TOKEN_FILE'))
|
||||||
if not token_file.exists():
|
if not token_file.exists():
|
||||||
app.logger.warning(
|
app.logger.warning(
|
||||||
|
|
|
@ -38,10 +38,7 @@ class ExtendedLoginForm(LoginForm):
|
||||||
authorized = super(ExtendedLoginForm, self).validate()
|
authorized = super(ExtendedLoginForm, self).validate()
|
||||||
|
|
||||||
if authorized:
|
if authorized:
|
||||||
current_app.logger.info(
|
current_app.logger.info(f"User with credentials '{self.email.data}' authorized through local database")
|
||||||
f"User with credentials '{self.email.data}' authorized "
|
|
||||||
f"through local database"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# run LDAP authorization
|
# run LDAP authorization
|
||||||
# if the authorization succeeds we also get the new_user_data
|
# if the authorization succeeds we also get the new_user_data
|
||||||
|
@ -74,41 +71,32 @@ class ExtendedLoginForm(LoginForm):
|
||||||
if authorized:
|
if authorized:
|
||||||
# if there was no user in the database before we create a new
|
# if there was no user in the database before we create a new
|
||||||
# user
|
# user
|
||||||
self.user = security.datastore.create_user(
|
self.user = security.datastore.create_user(username=new_user_data['username'], email=new_user_data['email'],
|
||||||
username=new_user_data['username'],
|
password=new_user_data['password'], roles=new_user_data['roles'])
|
||||||
email=new_user_data['email'],
|
|
||||||
password=new_user_data['password'],
|
|
||||||
roles=new_user_data['roles']
|
|
||||||
)
|
|
||||||
security.datastore.commit()
|
security.datastore.commit()
|
||||||
current_app.logger.info(
|
current_app.logger.info(f"New admin user '{new_user_data['username']} <{new_user_data['email']}>' created after"
|
||||||
f"New admin user '{new_user_data['username']} "
|
" successful LDAP authorization")
|
||||||
f"<{new_user_data['email']}>' created after successful "
|
|
||||||
f"LDAP authorization"
|
|
||||||
)
|
|
||||||
|
|
||||||
# if any of the authorization methods is successful we authorize
|
# if any of the authorization methods is successful we authorize the user
|
||||||
# the user
|
|
||||||
return authorized
|
return authorized
|
||||||
|
|
||||||
|
|
||||||
def validate_ldap(self):
|
def validate_ldap(self):
|
||||||
"""Validate the user and password through an LDAP server.
|
"""Validate the user and password through an LDAP server.
|
||||||
|
|
||||||
If the connection completes successfully the given user and password
|
If the connection completes successfully the given user and password is authorized.
|
||||||
is authorized. Then the permissions and additional information of the
|
Then the permissions and additional information of the user are obtained through an LDAP search.
|
||||||
user are obtained through an LDAP search.
|
The data is stored in a dict which will be used later to create/update the entry for the user in the local
|
||||||
The data is stored in a dict which will be used later to create/update
|
database.
|
||||||
the entry for the user in the local database.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
bool : result of the authorization process (True = success,
|
bool : result of the authorization process (True = success, False = failure)
|
||||||
False = failure)
|
dict : dictionary with information about an authorized user (contains username, email, hashed password,
|
||||||
dict : dictionary with information about an authorized user
|
roles)
|
||||||
(contains username, email, hashed password, roles)
|
|
||||||
"""
|
"""
|
||||||
ldap_server = ldap3.Server(current_app.config['LDAP_URL'])
|
ldap_server = ldap3.Server(current_app.config['LDAP_URL'])
|
||||||
ldap_user_group = current_app.config['LDAP_USER_GROUP']
|
ldap_user_group = current_app.config['LDAP_USER_GROUP']
|
||||||
|
@ -119,20 +107,15 @@ class ExtendedLoginForm(LoginForm):
|
||||||
password = self.password.data
|
password = self.password.data
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user = f"uid={username},ou={ldap_user_group},dc={ldap_domain}," \
|
user = f"uid={username},ou={ldap_user_group},dc={ldap_domain},dc={ldap_domain_ext}"
|
||||||
f"dc={ldap_domain_ext}"
|
con = ldap3.Connection(ldap_server,
|
||||||
con = ldap3.Connection(
|
user=user,
|
||||||
ldap_server,
|
password=password, auto_bind=True)
|
||||||
user=user,
|
|
||||||
password=password,
|
|
||||||
auto_bind=True
|
|
||||||
)
|
|
||||||
except ldap3.core.exceptions.LDAPBindError as e:
|
except ldap3.core.exceptions.LDAPBindError as e:
|
||||||
# server reachable but user unauthorized -> fail
|
# server reachable but user unauthorized -> fail
|
||||||
return False, None
|
return False, None
|
||||||
except LDAPSocketOpenError as e:
|
except LDAPSocketOpenError as e:
|
||||||
# server not reachable -> fail (but will try authorization from
|
# server not reachable -> fail (but will try authorization from local database later)
|
||||||
# local database later)
|
|
||||||
return False, None
|
return False, None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# for other Exceptions we just fail
|
# for other Exceptions we just fail
|
||||||
|
@ -144,18 +127,15 @@ class ExtendedLoginForm(LoginForm):
|
||||||
new_user_data['password'] = hash_password(password)
|
new_user_data['password'] = hash_password(password)
|
||||||
new_user_data['roles'] = []
|
new_user_data['roles'] = []
|
||||||
search_base = f"ou={ldap_user_group},dc={ldap_domain},dc={ldap_domain_ext}"
|
search_base = f"ou={ldap_user_group},dc={ldap_domain},dc={ldap_domain_ext}"
|
||||||
search_filter = f"(&(uid={username})(memberof=cn=Keyholders," \
|
search_filter = f"(&(uid={username})(memberof=cn=Keyholders,ou=Groups,dc={ldap_domain},dc={ldap_domain_ext}))"
|
||||||
f"ou=Groups,dc={ldap_domain},dc={ldap_domain_ext}))"
|
lock_permission = con.search(search_base, search_filter,
|
||||||
lock_permission = con.search(
|
attributes=ldap3.ALL_ATTRIBUTES)
|
||||||
search_base, search_filter, attributes=ldap3.ALL_ATTRIBUTES
|
|
||||||
)
|
|
||||||
|
|
||||||
if lock_permission:
|
if lock_permission:
|
||||||
new_user_data['email'] = con.entries[0].mail.value
|
new_user_data['email'] = con.entries[0].mail.value
|
||||||
else:
|
else:
|
||||||
return False, None
|
return False, None
|
||||||
search_filter = f'(&(uid={username})(memberof=cn=Vorstand,ou=Groups,' \
|
search_filter = f'(&(uid={username})(memberof=cn=Vorstand,ou=Groups,dc={ldap_domain},dc={ldap_domain_ext}))'
|
||||||
f'dc={ldap_domain},dc={ldap_domain_ext}))'
|
|
||||||
token_granting_permission = con.search(search_base, search_filter)
|
token_granting_permission = con.search(search_base, search_filter)
|
||||||
if token_granting_permission:
|
if token_granting_permission:
|
||||||
new_user_data['roles'].append('admin')
|
new_user_data['roles'].append('admin')
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
|
print("loading default config")
|
||||||
import bleach
|
import bleach
|
||||||
from flask_security import uia_email_mapper
|
from flask_security import uia_email_mapper
|
||||||
print("loading default config")
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
class DefaultConfig(object):
|
class DefaultConfig(object):
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
PORT = 80
|
|
||||||
|
|
||||||
SECRET_KEY = 'supersecret'
|
SECRET_KEY = 'supersecret'
|
||||||
SECURITY_PASSWORD_SALT = 'salty'
|
SECURITY_PASSWORD_SALT = 'salty'
|
||||||
|
@ -47,6 +44,7 @@ class DefaultConfig(object):
|
||||||
"pool_pre_ping": True,
|
"pool_pre_ping": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KEY_FILE = '/root/flask_keys'
|
||||||
TOKEN_FILE = "door_tokens"
|
TOKEN_FILE = "door_tokens"
|
||||||
ADMIN_FILE = 'admins'
|
ADMIN_FILE = 'admins'
|
||||||
|
|
||||||
|
|
|
@ -40,10 +40,12 @@ class DoorHandle:
|
||||||
self.mqtt_client.on_message = self.on_message
|
self.mqtt_client.on_message = self.on_message
|
||||||
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()
|
||||||
self.logger.info(f"Connected to MQTT broker at {mqtt_host}:{mqtt_port}")
|
|
||||||
|
|
||||||
self.data_fields = ['name', 'organization', 'email', 'valid_thru']
|
self.data_fields = ['name', 'organization', 'email', 'valid_thru']
|
||||||
|
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
# 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):
|
||||||
self.logger.info("Connected to MQTT broker with result code " + str(rc))
|
self.logger.info("Connected to MQTT broker with result code " + str(rc))
|
||||||
|
@ -52,6 +54,8 @@ class DoorHandle:
|
||||||
# reconnect then subscriptions will be renewed.
|
# reconnect then subscriptions will be renewed.
|
||||||
client.subscribe("door/#")
|
client.subscribe("door/#")
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
# The callback for when a PUBLISH message is received from the server.
|
# The callback for when a PUBLISH message is received from the server.
|
||||||
def on_message(self, client, userdata, msg):
|
def on_message(self, client, userdata, msg):
|
||||||
#print(msg.topic + " " + str(msg.payload))
|
#print(msg.topic + " " + str(msg.payload))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user