mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
More work on the authentication infrastructure
This commit is contained in:
parent
bdab1296a5
commit
08f6eb1c1f
@ -14,6 +14,7 @@ from threading import Lock
|
||||
|
||||
from calibre.db.cache import Cache
|
||||
from calibre.db.legacy import create_backend, LibraryDatabase
|
||||
from calibre.srv.auth import AuthController
|
||||
from calibre.srv.routes import Router
|
||||
from calibre.srv.users import UserManager
|
||||
from calibre.utils.date import utcnow
|
||||
@ -86,7 +87,7 @@ class Context(object):
|
||||
self.library_broker = LibraryBroker(libraries)
|
||||
self.testing = testing
|
||||
self.lock = Lock()
|
||||
self.user_manager = UserManager()
|
||||
self.user_manager = UserManager(opts.userdb)
|
||||
|
||||
def init_session(self, endpoint, data):
|
||||
pass
|
||||
@ -144,7 +145,13 @@ class Context(object):
|
||||
class Handler(object):
|
||||
|
||||
def __init__(self, libraries, opts, testing=False):
|
||||
self.router = Router(ctx=Context(libraries, opts, testing=testing), url_prefix=opts.url_prefix)
|
||||
ctx = Context(libraries, opts, testing=testing)
|
||||
self.auth_controller = None
|
||||
if opts.auth:
|
||||
has_ssl = opts.ssl_certfile is not None and opts.ssl_keyfile is not None
|
||||
prefer_basic_auth = {'auto':has_ssl, 'basic':True}.get(opts.auth_mode, 'digest')
|
||||
self.auth_controller = AuthController(user_credentials=ctx.user_manager, prefer_basic_auth=prefer_basic_auth)
|
||||
self.router = Router(ctx=ctx, url_prefix=opts.url_prefix, auth_controller=self.auth_controller)
|
||||
for module in ('content', 'ajax', 'code'):
|
||||
module = import_module('calibre.srv.' + module)
|
||||
self.router.load_routes(vars(module).itervalues())
|
||||
@ -154,6 +161,8 @@ class Handler(object):
|
||||
|
||||
def set_log(self, log):
|
||||
self.router.ctx.log = log
|
||||
if self.auth_controller is not None:
|
||||
self.auth_controller.log = log
|
||||
|
||||
def close(self):
|
||||
self.router.ctx.library_broker.close()
|
||||
|
@ -92,6 +92,22 @@ raw_options = (
|
||||
'max_log_size', 20,
|
||||
'The maximum size of log files, generated by the server. When the log becomes larger'
|
||||
' than this size, it is automatically rotated.',
|
||||
|
||||
'Enable/disable password based authentication to access the server',
|
||||
'auth', False,
|
||||
'By default, the server is unrestricted, allowing anyone to access it. You can'
|
||||
' restrict access to predefined users with this option.',
|
||||
|
||||
'Path to user database',
|
||||
'userdb', None,
|
||||
'Path to a file in which to store the user and password information. By default a'
|
||||
' file in the calibre configuration directory is used.',
|
||||
|
||||
'Choose the type of authentication used',
|
||||
'auth_mode', Choices('auto', 'basic', 'digest'),
|
||||
'Set the HTTP authentication mode used by the server. Set to "basic" is you are'
|
||||
' putting this server behind an SSL proxy. Otherwise, leave it as "auto", which'
|
||||
' will use "basic" if SSL is configured otherwise it will use "digest".',
|
||||
)
|
||||
assert len(raw_options) % 4 == 0
|
||||
|
||||
@ -105,7 +121,7 @@ def grouper(n, iterable, fillvalue=None):
|
||||
for shortdoc, name, default, doc in grouper(4, raw_options):
|
||||
choices = None
|
||||
if isinstance(default, Choices):
|
||||
choices = default
|
||||
choices = sorted(default)
|
||||
default = default.default
|
||||
options.append(Option(name, default, doc, shortdoc, choices))
|
||||
options = OrderedDict([(o.name, o) for o in sorted(options, key=attrgetter('name'))])
|
||||
|
@ -5,62 +5,71 @@
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
import os, json
|
||||
from threading import Lock
|
||||
|
||||
import apsw
|
||||
|
||||
from calibre.constants import config_dir
|
||||
from calibre.utils.config import to_json, from_json
|
||||
|
||||
|
||||
class UserManager(object):
|
||||
|
||||
lock = Lock()
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
if self._conn is None:
|
||||
self._conn = apsw.Connection(self.path)
|
||||
c = self._conn.cursor()
|
||||
uv = next(c.execute('PRAGMA foreign_keys = ON; PRAGMA user_version'))[0]
|
||||
if uv == 0:
|
||||
c.execute('''
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
hashed_pw TEXT NOT NULL,
|
||||
hash_type TEXT NOT NULL,
|
||||
creation_date INTEGER NOT NULL,
|
||||
UNIQUE(name)
|
||||
);
|
||||
with self._conn:
|
||||
c = self._conn.cursor()
|
||||
uv = next(c.execute('PRAGMA user_version'))[0]
|
||||
if uv == 0:
|
||||
# We have to store the unhashed password, since the digest
|
||||
# auth scheme requires it.
|
||||
# timestamp stores the ISO 8601 creation timestamp in UTC.
|
||||
c.execute('''
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
pw TEXT NOT NULL,
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
session_data TEXT NOT NULL DEFAULT "{}",
|
||||
restriction TEXT NOT NULL DEFAULT "",
|
||||
misc_data TEXT NOT NULL DEFAULT "{}",
|
||||
UNIQUE(name)
|
||||
);
|
||||
|
||||
CREATE TABLE session_data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user INTEGER NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
UNIQUE(user),
|
||||
FOREIGN KEY user REFERENCES users.id
|
||||
);
|
||||
PRAGMA user_version=1;
|
||||
''')
|
||||
c.close()
|
||||
|
||||
PRAGMA user_version=1;
|
||||
''')
|
||||
c.close()
|
||||
|
||||
def __init__(self):
|
||||
self.path = os.path.join(config_dir, 'server-users.sqlite')
|
||||
def __init__(self, path=None):
|
||||
self.path = os.path.join(config_dir, 'server-users.sqlite') if path is None else path
|
||||
self._conn = None
|
||||
|
||||
def get_session_data(self, username):
|
||||
for data, in self.conn.cursor().execute(
|
||||
'SELECT data FROM session_data INNER JOIN users ON (session_data.user = users.id) WHERE users.name=?', (username,)):
|
||||
try:
|
||||
return json.loads(data, object_hook=from_json)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
with self.lock:
|
||||
for data, in self.conn.cursor().execute(
|
||||
'SELECT data FROM users WHERE name=?', (username,)):
|
||||
try:
|
||||
return json.loads(data, object_hook=from_json)
|
||||
except Exception:
|
||||
break
|
||||
return {}
|
||||
|
||||
def set_session_data(self, username, data):
|
||||
conn = self.conn
|
||||
c = conn.cursor()
|
||||
for user_id, in c.execute('SELECT id FROM users WHERE name=?', (username,)):
|
||||
with self.lock:
|
||||
conn = self.conn
|
||||
c = conn.cursor()
|
||||
data = json.dumps(data, ensure_ascii=False, default=to_json)
|
||||
c.execute('UPDATE session_data SET data=? WHERE user=?', (data, user_id))
|
||||
if not conn.changes():
|
||||
c.execute('INSERT INTO session_data (data,user) VALUES (?,?)', (data, user_id))
|
||||
if isinstance(data, bytes):
|
||||
data = data.decode('utf-8')
|
||||
c.execute('UPDATE users SET data=? WHERE name=?', (data, username))
|
||||
|
||||
def get(self, username):
|
||||
' Get password for user, or None if user does not exist '
|
||||
with self.lock:
|
||||
for pw, in self.conn.cursor().execute(
|
||||
'SELECT pw FROM users WHERE name=?', (username,)):
|
||||
return pw
|
||||
|
@ -46,3 +46,9 @@ def ajax(path, on_complete, on_progress=None, bypass_cache=True, method='GET', q
|
||||
xhr.open(method, path)
|
||||
return xhr
|
||||
|
||||
# TODO: Implement AJAX based switch user by:
|
||||
# 1) POST to a logout URL with an incorrect username and password
|
||||
# 2) Have the server accept that incorrect username/password
|
||||
# 3) Navigate to / which should cause the browser to re-prompt for username and password
|
||||
# (only problem is login dialog will likely pre-show the wrong username) To workaround that,
|
||||
# You can consider using a dedicated ajax based login form, see http://www.peej.co.uk/articles/http-auth-with-html-forms.html)
|
||||
|
Loading…
x
Reference in New Issue
Block a user