mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Implement per-user library access restrictions
This commit is contained in:
parent
6549baf27e
commit
fa0fa3cba9
@ -50,3 +50,9 @@ class HTTPBadRequest(HTTPSimpleResponse):
|
||||
|
||||
def __init__(self, message, close_connection=False):
|
||||
HTTPSimpleResponse.__init__(self, httplib.BAD_REQUEST, message, close_connection)
|
||||
|
||||
|
||||
class HTTPForbidden(HTTPSimpleResponse):
|
||||
|
||||
def __init__(self, http_message='', close_connection=True):
|
||||
HTTPSimpleResponse.__init__(self, httplib.FORBIDDEN, http_message, close_connection)
|
||||
|
@ -1,19 +1,18 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
from __future__ import (unicode_literals, division, absolute_import,
|
||||
print_function)
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import json
|
||||
from functools import partial
|
||||
from importlib import import_module
|
||||
from threading import Lock
|
||||
|
||||
from calibre.srv.auth import AuthController
|
||||
from calibre.srv.errors import HTTPForbidden
|
||||
from calibre.srv.library_broker import LibraryBroker
|
||||
from calibre.srv.routes import Router
|
||||
from calibre.srv.users import UserManager
|
||||
from calibre.srv.library_broker import LibraryBroker
|
||||
from calibre.utils.date import utcnow
|
||||
|
||||
|
||||
@ -52,12 +51,25 @@ class Context(object):
|
||||
pass
|
||||
|
||||
def get_library(self, data, library_id=None):
|
||||
# TODO: Restrict the libraries based on data.username
|
||||
return self.library_broker.get(library_id)
|
||||
if not data.username:
|
||||
return self.library_broker.get(library_id)
|
||||
lf = partial(self.user_manager.allowed_library_names, data.username)
|
||||
allowed_libraries = self.library_broker.allowed_libraries(lf)
|
||||
if not allowed_libraries:
|
||||
raise HTTPForbidden('The user {} is not allowed to access any libraries on this server'.format(data.username))
|
||||
library_id = library_id or next(allowed_libraries.iterkeys())
|
||||
if library_id in allowed_libraries:
|
||||
return self.library_broker.get(library_id)
|
||||
raise HTTPForbidden('The user {} is not allowed to access the library {}'.format(data.username, library_id))
|
||||
|
||||
def library_info(self, data):
|
||||
# TODO: Restrict the libraries based on data.username
|
||||
return self.library_broker.library_map, self.library_broker.default_library
|
||||
if not data.username:
|
||||
return self.library_broker.library_map, self.library_broker.default_library
|
||||
lf = partial(self.user_manager.allowed_library_names, data.username)
|
||||
allowed_libraries = self.library_broker.allowed_libraries(lf)
|
||||
if not allowed_libraries:
|
||||
raise HTTPForbidden('The user {} is not allowed to access any libraries on this server'.format(data.username))
|
||||
return dict(allowed_libraries), next(allowed_libraries.iterkeys())
|
||||
|
||||
def allowed_book_ids(self, data, db):
|
||||
with self.lock:
|
||||
|
@ -104,7 +104,13 @@ class LibraryBroker(object):
|
||||
|
||||
@property
|
||||
def library_map(self):
|
||||
return {k: os.path.basename(v) for k, v in self.lmap.iteritems()}
|
||||
with self:
|
||||
return {k: os.path.basename(v) for k, v in self.lmap.iteritems()}
|
||||
|
||||
def allowed_libraries(self, filter_func):
|
||||
with self:
|
||||
allowed_names = filter_func(os.path.basename(l) for l in self.lmap.itervalues())
|
||||
return OrderedDict(((lid, path) for lid, path in self.lmap.iteritems() if os.path.basename(path) in allowed_names))
|
||||
|
||||
def __enter__(self):
|
||||
self.lock.acquire()
|
||||
|
@ -7,11 +7,14 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import httplib, base64, urllib2, subprocess, os, cookielib
|
||||
from collections import namedtuple
|
||||
try:
|
||||
from distutils.spawn import find_executable
|
||||
except ImportError: # windows
|
||||
find_executable = lambda x: None
|
||||
|
||||
from calibre.ptempfile import TemporaryDirectory
|
||||
from calibre.srv.errors import HTTPForbidden
|
||||
from calibre.srv.tests.base import BaseTest, TestServer
|
||||
from calibre.srv.routes import endpoint, Router
|
||||
|
||||
@ -116,6 +119,43 @@ class TestAuth(BaseTest):
|
||||
self.ae((httplib.UNAUTHORIZED, b''), request('asf', 'testpw'))
|
||||
# }}}
|
||||
|
||||
def test_library_restrictions(self): # {{{
|
||||
from calibre.srv.opts import Options
|
||||
from calibre.srv.handler import Handler
|
||||
from calibre.db.legacy import create_backend
|
||||
opts = Options(userdb=':memory:')
|
||||
Data = namedtuple('Data', 'username')
|
||||
with TemporaryDirectory() as base:
|
||||
l1, l2, l3 = map(lambda x: os.path.join(base, 'l' + x), '123')
|
||||
for l in (l1, l2, l3):
|
||||
create_backend(l).close()
|
||||
ctx = Handler((l1, l2, l3), opts).router.ctx
|
||||
um = ctx.user_manager
|
||||
|
||||
def get_library(username=None, library_id=None):
|
||||
ans = ctx.get_library(Data(username), library_id=library_id)
|
||||
return os.path.basename(ans.backend.library_path)
|
||||
|
||||
def library_info(username=None):
|
||||
lmap, defaultlib = ctx.library_info(Data(username))
|
||||
lmap = {k:os.path.basename(v) for k, v in lmap.iteritems()}
|
||||
return lmap, defaultlib
|
||||
|
||||
self.assertEqual(get_library(), 'l1')
|
||||
self.assertEqual(library_info()[0], {'l%d'%i:'l%d'%i for i in range(1, 4)})
|
||||
self.assertEqual(library_info()[1], 'l1')
|
||||
self.assertRaises(HTTPForbidden, get_library, 'xxx')
|
||||
um.add_user('a', 'a')
|
||||
self.assertEqual(library_info('a')[0], {'l%d'%i:'l%d'%i for i in range(1, 4)})
|
||||
um.update_user_restrictions('a', {'blocked_library_names': ['l2']})
|
||||
self.assertEqual(library_info('a')[0], {'l%d'%i:'l%d'%i for i in range(1, 4) if i != 2})
|
||||
um.update_user_restrictions('a', {'allowed_library_names': ['l3']})
|
||||
self.assertEqual(library_info('a')[0], {'l%d'%i:'l%d'%i for i in range(1, 4) if i == 3})
|
||||
self.assertEqual(library_info('a')[1], 'l3')
|
||||
self.assertRaises(HTTPForbidden, get_library, 'a', 'l1')
|
||||
|
||||
# }}}
|
||||
|
||||
def test_digest_auth(self): # {{{
|
||||
'Test HTTP Digest auth'
|
||||
from calibre.srv.http_request import normalize_header_name
|
||||
|
@ -61,6 +61,7 @@ class UserManager(object):
|
||||
def __init__(self, path=None):
|
||||
self.path = os.path.join(config_dir, 'server-users.sqlite') if path is None else path
|
||||
self._conn = None
|
||||
self._restrictions = {}
|
||||
|
||||
def get_session_data(self, username):
|
||||
with self.lock:
|
||||
@ -100,14 +101,17 @@ class UserManager(object):
|
||||
except ValueError:
|
||||
return _('The password must contain only ASCII (English) characters and symbols')
|
||||
|
||||
def add_user(self, username, pw, restriction='', readonly=False):
|
||||
def add_user(self, username, pw, restriction=None, readonly=False):
|
||||
with self.lock:
|
||||
msg = self.validate_username(username) or self.validate_password(pw)
|
||||
if msg is not None:
|
||||
raise ValueError(msg)
|
||||
restriction = restriction or {}
|
||||
if not isinstance(restriction, dict):
|
||||
raise TypeError('restriction must be a dict')
|
||||
self.conn.cursor().execute(
|
||||
'INSERT INTO users (name, pw, restriction, readonly) VALUES (?, ?, ?, ?)',
|
||||
(username, pw, restriction, ('y' if readonly else 'n')))
|
||||
(username, pw, json.dumps(restriction), ('y' if readonly else 'n')))
|
||||
|
||||
def remove_user(self, username):
|
||||
with self.lock:
|
||||
@ -127,3 +131,36 @@ class UserManager(object):
|
||||
raise ValueError(msg)
|
||||
self.conn.cursor().execute(
|
||||
'UPDATE users SET pw=? WHERE name=?', (pw, username))
|
||||
|
||||
def restrictions(self, username):
|
||||
with self.lock:
|
||||
r = self._restrictions.get(username)
|
||||
if r is None:
|
||||
for restriction, in self.conn.cursor().execute(
|
||||
'SELECT restriction FROM users WHERE name=?', (username,)):
|
||||
self._restrictions[username] = r = json.loads(restriction)
|
||||
r['allowed_library_names'] = frozenset(r.get('allowed_library_names', ()))
|
||||
r['blocked_library_names'] = frozenset(r.get('blocked_library_names', ()))
|
||||
break
|
||||
return r
|
||||
|
||||
def allowed_library_names(self, username, all_library_names):
|
||||
' Get allowed library names for specified user from set of all library names '
|
||||
r = self.restrictions(username)
|
||||
if r is None:
|
||||
return set()
|
||||
inc = r['allowed_library_names']
|
||||
exc = r['blocked_library_names']
|
||||
|
||||
def check(n):
|
||||
n = n.lower()
|
||||
return (not inc or n in inc) and n not in exc
|
||||
return {n for n in all_library_names if check(n)}
|
||||
|
||||
def update_user_restrictions(self, username, restrictions):
|
||||
if not isinstance(restrictions, dict):
|
||||
raise TypeError('restrictions must be a dict')
|
||||
with self.lock:
|
||||
self._restrictions.pop(username, None)
|
||||
self.conn.cursor().execute(
|
||||
'UPDATE users SET restriction=? WHERE name=?', (json.dumps(restrictions), username))
|
||||
|
Loading…
x
Reference in New Issue
Block a user