diff --git a/imaginaerraum_door_admin/webapp.py b/imaginaerraum_door_admin/webapp.py index 1b2f15b..6f03f6d 100644 --- a/imaginaerraum_door_admin/webapp.py +++ b/imaginaerraum_door_admin/webapp.py @@ -8,11 +8,12 @@ from flask_sqlalchemy import SQLAlchemy 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.forms import LoginForm, Required, PasswordField -from flask_security.utils import find_user +from flask_security.utils import find_user, verify_password from flask_mail import Mail from email_validator import validate_email import bleach import ldap3 +from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError from datetime import date from .door_handle import DoorHandle @@ -100,40 +101,61 @@ def create_application(config): # LDAP ldap_server = ldap3.Server(config.ldap_url) + local_ldap_cache = {} # dict for caching LDAP authorization locally (stores username + hashed password) def validate_ldap(user, password): - # try to connect to the LDAP server - # if the connection completes successfully the given user and password is authorized + """Validate the user and password through an LDAP server. + + If the connection completes successfully the given user and password is authorized and the password is stored + locally for future authorization without internet connectivity. + If the server is not reachable we check the password against a locally stored password (if the user previously + authorized through LDAP). + + Parameters + ---------- + user : username for the LDAP server + password : password for the LDAP server + + Returns + ------- + bool : result of the authorization process (True = success, False = failure) + """ + try: con = ldap3.Connection(ldap_server, user="uid=%s,ou=Users,dc=imaginaerraum,dc=de" % (user.username,), password=password, auto_bind=True) - except Exception: + except ldap3.core.exceptions.LDAPBindError: + # server reachable but user unauthorized -> fail return False - return con is not None + except LDAPSocketOpenError: + # server not reachable -> try cached authorization data + return user.username in local_ldap_cache and verify_password(password, local_ldap_cache[user.username]) + except Exception: + # for other Exceptions we just fail + return False + + # TODO check if user has permission to edit tokens + # if LDAP authorization succeeds we cache the password locally (in memory) to allow LDAP authentication even if + # the server is not reachable + local_ldap_cache[user.username] = hash_password(password) + return True class ExtendedLoginForm(LoginForm): email = StringField('Benutzername oder E-Mail', [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) + # try authorizing locally using Flask security user datastore + authorized = super(ExtendedLoginForm, self).validate() - 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 not authorized: + # try authorizing using LDAP + # authorization in LDAP uses username -> get username associated with email from the database + user = find_user(self.email.data) + authorized = validate_ldap(user, self.password.data) # 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') + return authorized # Setup Flask-Security user_datastore = SQLAlchemyUserDatastore(db, User, Role) diff --git a/setup.cfg b/setup.cfg index d024d78..88906c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = imaginaerraum_door_admin -version = 0.0.1 +version = 0.0.7 author = Telos4 author_email = simon.pirkelmann@gmail.com description = A simple web interface for our hackerspace's door token administration