mirror of
https://github.com/binary-kitchen/doorlockd
synced 2024-12-22 10:24:26 +01:00
doorlockd: First steps towards pydoorlock module
Extract Authenticator platform from doorlockd. Signed-off-by: Ralf Ramsauer <ralf@binary-kitchen.de>
This commit is contained in:
parent
7e26fa0cd6
commit
55205ad247
113
doorlockd
113
doorlockd
@ -17,14 +17,11 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|||||||
details.
|
details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import ldap
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from os.path import join
|
from os.path import join
|
||||||
from random import sample
|
|
||||||
from serial import Serial
|
from serial import Serial
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@ -36,6 +33,8 @@ from flask_wtf import FlaskForm
|
|||||||
from wtforms import PasswordField, StringField, SubmitField
|
from wtforms import PasswordField, StringField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Length
|
from wtforms.validators import DataRequired, Length
|
||||||
|
|
||||||
|
from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult
|
||||||
|
|
||||||
SYSCONFDIR = '.'
|
SYSCONFDIR = '.'
|
||||||
PREFIX = '.'
|
PREFIX = '.'
|
||||||
|
|
||||||
@ -66,7 +65,7 @@ webapp = Flask(__name__,
|
|||||||
webapp.config.from_pyfile(flask_config)
|
webapp.config.from_pyfile(flask_config)
|
||||||
socketio = SocketIO(webapp, async_mode='threading')
|
socketio = SocketIO(webapp, async_mode='threading')
|
||||||
serial_port = webapp.config.get('SERIAL_PORT')
|
serial_port = webapp.config.get('SERIAL_PORT')
|
||||||
simulate_ldap = webapp.config.get('SIMULATE_LDAP')
|
simulate_auth = webapp.config.get('SIMULATE_AUTH')
|
||||||
simulate_serial = webapp.config.get('SIMULATE_SERIAL')
|
simulate_serial = webapp.config.get('SIMULATE_SERIAL')
|
||||||
run_hooks = webapp.config.get('RUN_HOOKS')
|
run_hooks = webapp.config.get('RUN_HOOKS')
|
||||||
room = webapp.config.get('ROOM')
|
room = webapp.config.get('ROOM')
|
||||||
@ -76,9 +75,6 @@ file_local_db = webapp.config.get('LOCAL_USER_DB')
|
|||||||
|
|
||||||
html_title = '%s (%s - v%s)' % (title, __status__, __version__)
|
html_title = '%s (%s - v%s)' % (title, __status__, __version__)
|
||||||
|
|
||||||
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
|
|
||||||
ldap.set_option(ldap.OPT_REFERRALS, 0)
|
|
||||||
|
|
||||||
ldap_uri = webapp.config.get('LDAP_URI')
|
ldap_uri = webapp.config.get('LDAP_URI')
|
||||||
ldap_binddn = webapp.config.get('LDAP_BINDDN')
|
ldap_binddn = webapp.config.get('LDAP_BINDDN')
|
||||||
|
|
||||||
@ -101,32 +97,6 @@ host = 'localhost'
|
|||||||
if webapp.config.get('DEBUG'):
|
if webapp.config.get('DEBUG'):
|
||||||
host = '0.0.0.0'
|
host = '0.0.0.0'
|
||||||
|
|
||||||
# copied from sudo
|
|
||||||
eperm_insults = {
|
|
||||||
'Wrong! You cheating scum!',
|
|
||||||
'And you call yourself a Rocket Scientist!',
|
|
||||||
'No soap, honkie-lips.',
|
|
||||||
'Where did you learn to type?',
|
|
||||||
'Are you on drugs?',
|
|
||||||
'My pet ferret can type better than you!',
|
|
||||||
'You type like i drive.',
|
|
||||||
'Do you think like you type?',
|
|
||||||
'Your mind just hasn\'t been the same since the electro-shock, has it?',
|
|
||||||
'Maybe if you used more than just two fingers...',
|
|
||||||
'BOB says: You seem to have forgotten your passwd, enter another!',
|
|
||||||
'stty: unknown mode: doofus',
|
|
||||||
'I can\'t hear you -- I\'m using the scrambler.',
|
|
||||||
'The more you drive -- the dumber you get.',
|
|
||||||
'Listen, broccoli brains, I don\'t have time to listen to this trash.',
|
|
||||||
'I\'ve seen penguins that can type better than that.',
|
|
||||||
'Have you considered trying to match wits with a rutabaga?',
|
|
||||||
'You speak an infinite deal of nothing',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def choose_insult():
|
|
||||||
return(sample(eperm_insults, 1)[0])
|
|
||||||
|
|
||||||
|
|
||||||
def playsound(filename):
|
def playsound(filename):
|
||||||
if not sounds:
|
if not sounds:
|
||||||
@ -162,11 +132,6 @@ def sound_helper(old_state, new_state, button):
|
|||||||
playsound(wave_lock)
|
playsound(wave_lock)
|
||||||
|
|
||||||
|
|
||||||
class AuthMethod(Enum):
|
|
||||||
LDAP_USER_PW = 1
|
|
||||||
LOCAL_USER_DB = 2
|
|
||||||
|
|
||||||
|
|
||||||
class DoorState(Enum):
|
class DoorState(Enum):
|
||||||
# These numbers are used by the App since version 3.0, do NOT change them
|
# These numbers are used by the App since version 3.0, do NOT change them
|
||||||
Open = 0
|
Open = 0
|
||||||
@ -211,14 +176,13 @@ class LogicResponse(Enum):
|
|||||||
# don't break old apps, value 3 is reserved now
|
# don't break old apps, value 3 is reserved now
|
||||||
RESERVED = 3
|
RESERVED = 3
|
||||||
Inval = 4
|
Inval = 4
|
||||||
LDAP = 5
|
|
||||||
|
|
||||||
EmergencyUnlock = 10,
|
EmergencyUnlock = 10,
|
||||||
ButtonLock = 11,
|
ButtonLock = 11,
|
||||||
ButtonUnlock = 12,
|
ButtonUnlock = 12,
|
||||||
ButtonPresent = 13,
|
ButtonPresent = 13,
|
||||||
|
|
||||||
def to_html(self):
|
def __str__(self):
|
||||||
if self == LogicResponse.Success:
|
if self == LogicResponse.Success:
|
||||||
return 'Yo, passt.'
|
return 'Yo, passt.'
|
||||||
elif self == LogicResponse.Perm:
|
elif self == LogicResponse.Perm:
|
||||||
@ -227,8 +191,6 @@ class LogicResponse(Enum):
|
|||||||
return 'Zustand bereits aktiv'
|
return 'Zustand bereits aktiv'
|
||||||
elif self == LogicResponse.Inval:
|
elif self == LogicResponse.Inval:
|
||||||
return 'Das was du willst geht nicht.'
|
return 'Das was du willst geht nicht.'
|
||||||
elif self == LogicResponse.LDAP:
|
|
||||||
return 'Moep! Geh LDAP fixen!'
|
|
||||||
elif self == LogicResponse.EmergencyUnlock:
|
elif self == LogicResponse.EmergencyUnlock:
|
||||||
return '!!! Emergency Unlock !!!'
|
return '!!! Emergency Unlock !!!'
|
||||||
elif self == LogicResponse.ButtonLock:
|
elif self == LogicResponse.ButtonLock:
|
||||||
@ -238,7 +200,7 @@ class LogicResponse(Enum):
|
|||||||
elif self == LogicResponse.ButtonPresent:
|
elif self == LogicResponse.ButtonPresent:
|
||||||
return 'Present by button'
|
return 'Present by button'
|
||||||
|
|
||||||
return 'Bitte spezifizieren Sie.'
|
return 'Error'
|
||||||
|
|
||||||
|
|
||||||
class DoorHandler:
|
class DoorHandler:
|
||||||
@ -344,61 +306,19 @@ class DoorHandler:
|
|||||||
|
|
||||||
class Logic:
|
class Logic:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.auth = Authenticator(simulate_auth)
|
||||||
|
if ldap_uri and ldap_binddn:
|
||||||
|
log.info('Initialising LDAP auth backend')
|
||||||
|
self.auth.enable_ldap_backend(ldap_uri, ldap_binddn)
|
||||||
|
if file_local_db:
|
||||||
|
log.info('Initialising local auth backend')
|
||||||
|
self.auth.enable_local_backend(file_local_db)
|
||||||
|
|
||||||
self.door_handler = DoorHandler(serial_port)
|
self.door_handler = DoorHandler(serial_port)
|
||||||
self.local_db = dict()
|
|
||||||
|
|
||||||
if not file_local_db:
|
|
||||||
return
|
|
||||||
|
|
||||||
with open(file_local_db, 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
line = line.split()
|
|
||||||
user = line[0]
|
|
||||||
pwd = line[1].split(':')
|
|
||||||
self.local_db[user] = pwd
|
|
||||||
|
|
||||||
def _try_auth_local(self, user, password):
|
|
||||||
if user not in self.local_db:
|
|
||||||
return LogicResponse.Perm
|
|
||||||
|
|
||||||
stored_pw = self.local_db[user][0]
|
|
||||||
stored_salt = self.local_db[user][1]
|
|
||||||
if stored_pw == hashlib.sha256(stored_salt.encode() + password.encode()).hexdigest():
|
|
||||||
return LogicResponse.Success
|
|
||||||
|
|
||||||
return LogicResponse.Perm
|
|
||||||
|
|
||||||
def _try_auth_ldap(self, user, password):
|
|
||||||
if simulate_ldap:
|
|
||||||
log.info('SIMULATION MODE! ACCEPTING ANYTHING!')
|
|
||||||
return LogicResponse.Success
|
|
||||||
|
|
||||||
log.info(' Trying to LDAP auth (user, password) as user %s', user)
|
|
||||||
ldap_username = ldap_binddn % user
|
|
||||||
try:
|
|
||||||
l = ldap.initialize(ldap_uri)
|
|
||||||
l.simple_bind_s(ldap_username, password)
|
|
||||||
l.unbind_s()
|
|
||||||
except ldap.INVALID_CREDENTIALS:
|
|
||||||
log.info(' Invalid credentials')
|
|
||||||
return LogicResponse.Perm
|
|
||||||
except ldap.LDAPError as e:
|
|
||||||
log.info(' LDAP Error: %s' % e)
|
|
||||||
return LogicResponse.LDAP
|
|
||||||
return LogicResponse.Success
|
|
||||||
|
|
||||||
def try_auth(self, credentials):
|
|
||||||
method = credentials[0]
|
|
||||||
if method == AuthMethod.LDAP_USER_PW:
|
|
||||||
return self._try_auth_ldap(credentials[1], credentials[2])
|
|
||||||
elif method == AuthMethod.LOCAL_USER_DB:
|
|
||||||
return self._try_auth_local(credentials[1], credentials[2])
|
|
||||||
|
|
||||||
return LogicResponse.Inval
|
|
||||||
|
|
||||||
def _request(self, state, credentials):
|
def _request(self, state, credentials):
|
||||||
err = self.try_auth(credentials)
|
err = self.auth.try_auth(credentials)
|
||||||
if err != LogicResponse.Success:
|
if err != AuthenticationResult.Success:
|
||||||
return err
|
return err
|
||||||
return self.door_handler.request(state)
|
return self.door_handler.request(state)
|
||||||
|
|
||||||
@ -415,7 +335,7 @@ class Logic:
|
|||||||
if message is None:
|
if message is None:
|
||||||
message = self.state.to_html()
|
message = self.state.to_html()
|
||||||
else:
|
else:
|
||||||
message = message.to_html()
|
message = str(message)
|
||||||
|
|
||||||
socketio.emit('status', {'led': led, 'message': message})
|
socketio.emit('status', {'led': led, 'message': message})
|
||||||
|
|
||||||
@ -541,6 +461,7 @@ def home():
|
|||||||
|
|
||||||
return render_template('index.html',
|
return render_template('index.html',
|
||||||
authentication_form=authentication_form,
|
authentication_form=authentication_form,
|
||||||
|
auth_backends=logic.auth.backends,
|
||||||
response=response,
|
response=response,
|
||||||
state_text=logic.state.to_html(),
|
state_text=logic.state.to_html(),
|
||||||
led=logic.state.to_img(),
|
led=logic.state.to_img(),
|
||||||
|
148
pydoorlock/Authenticator.py
Normal file
148
pydoorlock/Authenticator.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"""
|
||||||
|
Doorlockd -- Binary Kitchen's smart door opener
|
||||||
|
|
||||||
|
Copyright (c) Binary Kitchen e.V., 2018
|
||||||
|
|
||||||
|
Author:
|
||||||
|
Ralf Ramsauer <ralf@binary-kitchen.de>
|
||||||
|
|
||||||
|
This work is licensed under the terms of the GNU GPL, version 2. See
|
||||||
|
the LICENSE file in the top-level directory.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import ldap
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from random import sample
|
||||||
|
|
||||||
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
# copied from sudo
|
||||||
|
eperm_insults = {
|
||||||
|
'Wrong! You cheating scum!',
|
||||||
|
'And you call yourself a Rocket Scientist!',
|
||||||
|
'No soap, honkie-lips.',
|
||||||
|
'Where did you learn to type?',
|
||||||
|
'Are you on drugs?',
|
||||||
|
'My pet ferret can type better than you!',
|
||||||
|
'You type like i drive.',
|
||||||
|
'Do you think like you type?',
|
||||||
|
'Your mind just hasn\'t been the same since the electro-shock, has it?',
|
||||||
|
'Maybe if you used more than just two fingers...',
|
||||||
|
'BOB says: You seem to have forgotten your passwd, enter another!',
|
||||||
|
'stty: unknown mode: doofus',
|
||||||
|
'I can\'t hear you -- I\'m using the scrambler.',
|
||||||
|
'The more you drive -- the dumber you get.',
|
||||||
|
'Listen, broccoli brains, I don\'t have time to listen to this trash.',
|
||||||
|
'I\'ve seen penguins that can type better than that.',
|
||||||
|
'Have you considered trying to match wits with a rutabaga?',
|
||||||
|
'You speak an infinite deal of nothing',
|
||||||
|
}
|
||||||
|
|
||||||
|
def choose_insult():
|
||||||
|
return(sample(eperm_insults, 1)[0])
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMethod(Enum):
|
||||||
|
LDAP_USER_PW = 1
|
||||||
|
LOCAL_USER_DB = 2
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self == AuthMethod.LDAP_USER_PW:
|
||||||
|
return 'LDAP'
|
||||||
|
elif self == AuthMethod.LOCAL_USER_DB:
|
||||||
|
return 'Local'
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticationResult(Enum):
|
||||||
|
Success = 0
|
||||||
|
Perm = 1
|
||||||
|
InternalError = 2
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self == AuthenticationResult.Success:
|
||||||
|
return 'Yo, passt!'
|
||||||
|
elif self == AuthenticationResult.Perm:
|
||||||
|
return choose_insult()
|
||||||
|
else:
|
||||||
|
return 'Internal authentication error'
|
||||||
|
|
||||||
|
class Authenticator:
|
||||||
|
def __init__(self, simulate=False):
|
||||||
|
self._simulate = simulate
|
||||||
|
self._backends = set()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backends(self):
|
||||||
|
return self._backends
|
||||||
|
|
||||||
|
def enable_ldap_backend(self, uri, binddn):
|
||||||
|
self._ldap_uri = uri
|
||||||
|
self._ldap_binddn = binddn
|
||||||
|
self._backends.add(AuthMethod.LDAP_USER_PW)
|
||||||
|
|
||||||
|
ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
|
||||||
|
ldap.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def enable_local_backend(self, filename):
|
||||||
|
self._local_db = dict()
|
||||||
|
|
||||||
|
with open(filename, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.split()
|
||||||
|
user = line[0]
|
||||||
|
pwd = line[1].split(':')
|
||||||
|
self._local_db[user] = pwd
|
||||||
|
|
||||||
|
self._backends.add(AuthMethod.LOCAL_USER_DB)
|
||||||
|
|
||||||
|
def _try_auth_local(self, user, password):
|
||||||
|
if user not in self._local_db:
|
||||||
|
return AuthenticationResult.Perm
|
||||||
|
|
||||||
|
stored_pw = self._local_db[user][0]
|
||||||
|
stored_salt = self._local_db[user][1]
|
||||||
|
if stored_pw == hashlib.sha256(stored_salt.encode() + password.encode()).hexdigest():
|
||||||
|
return AuthenticationResult.Success
|
||||||
|
|
||||||
|
return AuthenticationResult.Perm
|
||||||
|
|
||||||
|
def _try_auth_ldap(self, user, password):
|
||||||
|
log.info(' Trying to LDAP auth (user, password) as user %s', user)
|
||||||
|
ldap_username = self._ldap_binddn % user
|
||||||
|
try:
|
||||||
|
l = ldap.initialize(self._ldap_uri)
|
||||||
|
l.simple_bind_s(ldap_username, password)
|
||||||
|
l.unbind_s()
|
||||||
|
except ldap.INVALID_CREDENTIALS:
|
||||||
|
log.info(' Invalid credentials')
|
||||||
|
return AuthenticationResult.Perm
|
||||||
|
except ldap.LDAPError as e:
|
||||||
|
log.info(' LDAP Error: %s' % e)
|
||||||
|
return AuthenticationResult.InternalError
|
||||||
|
return AuthenticationResult.Success
|
||||||
|
|
||||||
|
def try_auth(self, credentials):
|
||||||
|
if self._simulate:
|
||||||
|
log.info('SIMULATION MODE! ACCEPTING ANYTHING!')
|
||||||
|
return AuthenticationResult.Success
|
||||||
|
|
||||||
|
method = credentials[0]
|
||||||
|
if method not in self._backends:
|
||||||
|
return AuthenticationResult.InternalError
|
||||||
|
|
||||||
|
if method == AuthMethod.LDAP_USER_PW:
|
||||||
|
return self._try_auth_ldap(credentials[1], credentials[2])
|
||||||
|
elif method == AuthMethod.LOCAL_USER_DB:
|
||||||
|
return self._try_auth_local(credentials[1], credentials[2])
|
||||||
|
|
||||||
|
return AuthenticationResult.InternalError
|
0
pydoorlock/__init__.py
Normal file
0
pydoorlock/__init__.py
Normal file
@ -26,8 +26,9 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="method">Authentication method</label>
|
<label for="method">Authentication method</label>
|
||||||
<select name="method" id="method" class="form-control">
|
<select name="method" id="method" class="form-control">
|
||||||
<option selected>LDAP</option>
|
{% for backend in auth_backends %}
|
||||||
<option>Local</option>
|
<option>{{ backend }}</option>
|
||||||
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<input class="btn btn-success btn-lg btn-block" id="open" name="open" type="submit" value="Open">
|
<input class="btn btn-success btn-lg btn-block" id="open" name="open" type="submit" value="Open">
|
||||||
@ -36,7 +37,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% if response %}
|
{% if response %}
|
||||||
<hr/>
|
<hr/>
|
||||||
<h1>{{ response.to_html() }}</h1>
|
<h1>{{ response }}</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<hr/>
|
<hr/>
|
||||||
Die Kitchen ist: {{ state_text }}
|
Die Kitchen ist: {{ state_text }}
|
||||||
|
Loading…
Reference in New Issue
Block a user