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:
Ralf Ramsauer 2018-10-08 01:12:32 +02:00
parent 7e26fa0cd6
commit 55205ad247
4 changed files with 169 additions and 99 deletions

113
doorlockd
View File

@ -17,14 +17,11 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
"""
import hashlib
import ldap
import logging
import sys
from enum import Enum
from os.path import join
from random import sample
from serial import Serial
from subprocess import Popen
from threading import Thread
@ -36,6 +33,8 @@ from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Length
from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult
SYSCONFDIR = '.'
PREFIX = '.'
@ -66,7 +65,7 @@ webapp = Flask(__name__,
webapp.config.from_pyfile(flask_config)
socketio = SocketIO(webapp, async_mode='threading')
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')
run_hooks = webapp.config.get('RUN_HOOKS')
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__)
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_binddn = webapp.config.get('LDAP_BINDDN')
@ -101,32 +97,6 @@ host = 'localhost'
if webapp.config.get('DEBUG'):
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):
if not sounds:
@ -162,11 +132,6 @@ def sound_helper(old_state, new_state, button):
playsound(wave_lock)
class AuthMethod(Enum):
LDAP_USER_PW = 1
LOCAL_USER_DB = 2
class DoorState(Enum):
# These numbers are used by the App since version 3.0, do NOT change them
Open = 0
@ -211,14 +176,13 @@ class LogicResponse(Enum):
# don't break old apps, value 3 is reserved now
RESERVED = 3
Inval = 4
LDAP = 5
EmergencyUnlock = 10,
ButtonLock = 11,
ButtonUnlock = 12,
ButtonPresent = 13,
def to_html(self):
def __str__(self):
if self == LogicResponse.Success:
return 'Yo, passt.'
elif self == LogicResponse.Perm:
@ -227,8 +191,6 @@ class LogicResponse(Enum):
return 'Zustand bereits aktiv'
elif self == LogicResponse.Inval:
return 'Das was du willst geht nicht.'
elif self == LogicResponse.LDAP:
return 'Moep! Geh LDAP fixen!'
elif self == LogicResponse.EmergencyUnlock:
return '!!! Emergency Unlock !!!'
elif self == LogicResponse.ButtonLock:
@ -238,7 +200,7 @@ class LogicResponse(Enum):
elif self == LogicResponse.ButtonPresent:
return 'Present by button'
return 'Bitte spezifizieren Sie.'
return 'Error'
class DoorHandler:
@ -344,61 +306,19 @@ class DoorHandler:
class Logic:
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.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):
err = self.try_auth(credentials)
if err != LogicResponse.Success:
err = self.auth.try_auth(credentials)
if err != AuthenticationResult.Success:
return err
return self.door_handler.request(state)
@ -415,7 +335,7 @@ class Logic:
if message is None:
message = self.state.to_html()
else:
message = message.to_html()
message = str(message)
socketio.emit('status', {'led': led, 'message': message})
@ -541,6 +461,7 @@ def home():
return render_template('index.html',
authentication_form=authentication_form,
auth_backends=logic.auth.backends,
response=response,
state_text=logic.state.to_html(),
led=logic.state.to_img(),

148
pydoorlock/Authenticator.py Normal file
View 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
View File

View File

@ -26,8 +26,9 @@
<div class="form-group">
<label for="method">Authentication method</label>
<select name="method" id="method" class="form-control">
<option selected>LDAP</option>
<option>Local</option>
{% for backend in auth_backends %}
<option>{{ backend }}</option>
{% endfor %}
</select>
</div>
<input class="btn btn-success btn-lg btn-block" id="open" name="open" type="submit" value="Open">
@ -36,7 +37,7 @@
</form>
{% if response %}
<hr/>
<h1>{{ response.to_html() }}</h1>
<h1>{{ response }}</h1>
{% endif %}
<hr/>
Die Kitchen ist: {{ state_text }}