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.cache import Cache
|
||||||
from calibre.db.legacy import create_backend, LibraryDatabase
|
from calibre.db.legacy import create_backend, LibraryDatabase
|
||||||
|
from calibre.srv.auth import AuthController
|
||||||
from calibre.srv.routes import Router
|
from calibre.srv.routes import Router
|
||||||
from calibre.srv.users import UserManager
|
from calibre.srv.users import UserManager
|
||||||
from calibre.utils.date import utcnow
|
from calibre.utils.date import utcnow
|
||||||
@ -86,7 +87,7 @@ class Context(object):
|
|||||||
self.library_broker = LibraryBroker(libraries)
|
self.library_broker = LibraryBroker(libraries)
|
||||||
self.testing = testing
|
self.testing = testing
|
||||||
self.lock = Lock()
|
self.lock = Lock()
|
||||||
self.user_manager = UserManager()
|
self.user_manager = UserManager(opts.userdb)
|
||||||
|
|
||||||
def init_session(self, endpoint, data):
|
def init_session(self, endpoint, data):
|
||||||
pass
|
pass
|
||||||
@ -144,7 +145,13 @@ class Context(object):
|
|||||||
class Handler(object):
|
class Handler(object):
|
||||||
|
|
||||||
def __init__(self, libraries, opts, testing=False):
|
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'):
|
for module in ('content', 'ajax', 'code'):
|
||||||
module = import_module('calibre.srv.' + module)
|
module = import_module('calibre.srv.' + module)
|
||||||
self.router.load_routes(vars(module).itervalues())
|
self.router.load_routes(vars(module).itervalues())
|
||||||
@ -154,6 +161,8 @@ class Handler(object):
|
|||||||
|
|
||||||
def set_log(self, log):
|
def set_log(self, log):
|
||||||
self.router.ctx.log = log
|
self.router.ctx.log = log
|
||||||
|
if self.auth_controller is not None:
|
||||||
|
self.auth_controller.log = log
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.router.ctx.library_broker.close()
|
self.router.ctx.library_broker.close()
|
||||||
|
@ -92,6 +92,22 @@ raw_options = (
|
|||||||
'max_log_size', 20,
|
'max_log_size', 20,
|
||||||
'The maximum size of log files, generated by the server. When the log becomes larger'
|
'The maximum size of log files, generated by the server. When the log becomes larger'
|
||||||
' than this size, it is automatically rotated.',
|
' 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
|
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):
|
for shortdoc, name, default, doc in grouper(4, raw_options):
|
||||||
choices = None
|
choices = None
|
||||||
if isinstance(default, Choices):
|
if isinstance(default, Choices):
|
||||||
choices = default
|
choices = sorted(default)
|
||||||
default = default.default
|
default = default.default
|
||||||
options.append(Option(name, default, doc, shortdoc, choices))
|
options.append(Option(name, default, doc, shortdoc, choices))
|
||||||
options = OrderedDict([(o.name, o) for o in sorted(options, key=attrgetter('name'))])
|
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,
|
from __future__ import (unicode_literals, division, absolute_import,
|
||||||
print_function)
|
print_function)
|
||||||
import os, json
|
import os, json
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
import apsw
|
import apsw
|
||||||
|
|
||||||
from calibre.constants import config_dir
|
from calibre.constants import config_dir
|
||||||
from calibre.utils.config import to_json, from_json
|
from calibre.utils.config import to_json, from_json
|
||||||
|
|
||||||
|
|
||||||
class UserManager(object):
|
class UserManager(object):
|
||||||
|
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def conn(self):
|
def conn(self):
|
||||||
if self._conn is None:
|
if self._conn is None:
|
||||||
self._conn = apsw.Connection(self.path)
|
self._conn = apsw.Connection(self.path)
|
||||||
c = self._conn.cursor()
|
with self._conn:
|
||||||
uv = next(c.execute('PRAGMA foreign_keys = ON; PRAGMA user_version'))[0]
|
c = self._conn.cursor()
|
||||||
if uv == 0:
|
uv = next(c.execute('PRAGMA user_version'))[0]
|
||||||
c.execute('''
|
if uv == 0:
|
||||||
CREATE TABLE users (
|
# We have to store the unhashed password, since the digest
|
||||||
id INTEGER PRIMARY KEY,
|
# auth scheme requires it.
|
||||||
name TEXT NOT NULL,
|
# timestamp stores the ISO 8601 creation timestamp in UTC.
|
||||||
salt TEXT NOT NULL,
|
c.execute('''
|
||||||
hashed_pw TEXT NOT NULL,
|
CREATE TABLE users (
|
||||||
hash_type TEXT NOT NULL,
|
id INTEGER PRIMARY KEY,
|
||||||
creation_date INTEGER NOT NULL,
|
name TEXT NOT NULL,
|
||||||
UNIQUE(name)
|
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 (
|
PRAGMA user_version=1;
|
||||||
id INTEGER PRIMARY KEY,
|
''')
|
||||||
user INTEGER NOT NULL,
|
c.close()
|
||||||
data TEXT NOT NULL,
|
|
||||||
UNIQUE(user),
|
|
||||||
FOREIGN KEY user REFERENCES users.id
|
|
||||||
);
|
|
||||||
|
|
||||||
PRAGMA user_version=1;
|
def __init__(self, path=None):
|
||||||
''')
|
self.path = os.path.join(config_dir, 'server-users.sqlite') if path is None else path
|
||||||
c.close()
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.path = os.path.join(config_dir, 'server-users.sqlite')
|
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
|
||||||
def get_session_data(self, username):
|
def get_session_data(self, username):
|
||||||
for data, in self.conn.cursor().execute(
|
with self.lock:
|
||||||
'SELECT data FROM session_data INNER JOIN users ON (session_data.user = users.id) WHERE users.name=?', (username,)):
|
for data, in self.conn.cursor().execute(
|
||||||
try:
|
'SELECT data FROM users WHERE name=?', (username,)):
|
||||||
return json.loads(data, object_hook=from_json)
|
try:
|
||||||
except Exception:
|
return json.loads(data, object_hook=from_json)
|
||||||
pass
|
except Exception:
|
||||||
return {}
|
break
|
||||||
|
return {}
|
||||||
|
|
||||||
def set_session_data(self, username, data):
|
def set_session_data(self, username, data):
|
||||||
conn = self.conn
|
with self.lock:
|
||||||
c = conn.cursor()
|
conn = self.conn
|
||||||
for user_id, in c.execute('SELECT id FROM users WHERE name=?', (username,)):
|
c = conn.cursor()
|
||||||
data = json.dumps(data, ensure_ascii=False, default=to_json)
|
data = json.dumps(data, ensure_ascii=False, default=to_json)
|
||||||
c.execute('UPDATE session_data SET data=? WHERE user=?', (data, user_id))
|
if isinstance(data, bytes):
|
||||||
if not conn.changes():
|
data = data.decode('utf-8')
|
||||||
c.execute('INSERT INTO session_data (data,user) VALUES (?,?)', (data, user_id))
|
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)
|
xhr.open(method, path)
|
||||||
return xhr
|
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