changed priority for authorization (Flask first, LDAP second) and cache LDAP credentials in memory on successful authentication

This commit is contained in:
Simon Pirkelmann 2021-03-22 23:42:29 +01:00
parent 2aa958aaa0
commit eb4c027f46
2 changed files with 43 additions and 21 deletions

View File

@ -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 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_security.utils import find_user 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 bleach import bleach
import ldap3 import ldap3
from ldap3.core.exceptions import LDAPBindError, LDAPSocketOpenError
from datetime import date from datetime import date
from .door_handle import DoorHandle from .door_handle import DoorHandle
@ -100,40 +101,61 @@ def create_application(config):
# LDAP # LDAP
ldap_server = ldap3.Server(config.ldap_url) 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): def validate_ldap(user, password):
# try to connect to the LDAP server """Validate the user and password through an LDAP server.
# if the connection completes successfully the given user and password is authorized
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: try:
con = ldap3.Connection(ldap_server, user="uid=%s,ou=Users,dc=imaginaerraum,dc=de" % (user.username,), con = ldap3.Connection(ldap_server, user="uid=%s,ou=Users,dc=imaginaerraum,dc=de" % (user.username,),
password=password, auto_bind=True) password=password, auto_bind=True)
except Exception: except ldap3.core.exceptions.LDAPBindError:
# server reachable but user unauthorized -> fail
return False 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): 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): def validate(self):
# try authorizing locally using Flask security user datastore
authorized = super(ExtendedLoginForm, self).validate()
if not authorized:
# try authorizing using LDAP
# 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)
# try authorizing using LDAP authorized = validate_ldap(user, self.password.data)
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 # if any of the authorization methods is successful we authorize the user
return response_ldap or response_orig return authorized
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)

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = imaginaerraum_door_admin name = imaginaerraum_door_admin
version = 0.0.1 version = 0.0.7
author = Telos4 author = Telos4
author_email = simon.pirkelmann@gmail.com author_email = simon.pirkelmann@gmail.com
description = A simple web interface for our hackerspace's door token administration description = A simple web interface for our hackerspace's door token administration