mirror of
https://github.com/binary-kitchen/doorlockd
synced 2024-12-22 02:14: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.
|
||||
"""
|
||||
|
||||
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
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">
|
||||
<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 }}
|
||||
|
Loading…
Reference in New Issue
Block a user