%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/ |
| Current File : //lib/calibre/calibre/srv/users.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
import apsw
import json
import os
import re
from functools import lru_cache
from threading import RLock
from calibre import as_unicode
from calibre.constants import config_dir
from calibre.utils.config import from_json, to_json
from polyglot.builtins import iteritems
def as_json(data):
return json.dumps(data, ensure_ascii=False, default=to_json)
def load_json(raw):
try:
return json.loads(raw, object_hook=from_json)
except Exception:
return {}
@lru_cache(maxsize=1024)
def parse_restriction(raw):
r = load_json(raw)
if not isinstance(r, dict):
r = {}
lr = r.get('library_restrictions', {})
if not isinstance(lr, dict):
lr = {}
r['allowed_library_names'] = frozenset(map(lambda x: x.lower(), r.get('allowed_library_names', ())))
r['blocked_library_names'] = frozenset(map(lambda x: x.lower(), r.get('blocked_library_names', ())))
r['library_restrictions'] = {k.lower(): v or '' for k, v in iteritems(lr)}
return r
def serialize_restriction(r):
ans = {}
for x in 'allowed_library_names blocked_library_names'.split():
v = r.get(x)
if v:
ans[x] = list(v)
ans['library_restrictions'] = {l.lower(): v or '' for l, v in iteritems(r.get('library_restrictions', {}))}
return json.dumps(ans)
def validate_username(username):
if re.sub(r'[-a-zA-Z_0-9 ]', '', username):
return _('For maximum compatibility you should use only the letters A-Z,'
' the numbers 0-9, spaces, underscores and hyphens in the username')
def validate_password(pw):
if not pw:
return _('Empty passwords are not allowed')
try:
pw = pw.encode('ascii', 'strict')
except ValueError:
return _('The password must contain only ASCII (English) characters and symbols')
def create_user_data(pw, readonly=False, restriction=None):
return {
'pw':pw, 'restriction':parse_restriction(restriction or '{}').copy(), 'readonly': readonly
}
def connect(path, exc_class=ValueError):
try:
return apsw.Connection(path)
except apsw.CantOpenError as e:
pdir = os.path.dirname(path)
if os.path.isdir(pdir):
raise exc_class(f'Failed to open userdb database at {path} with error: {as_unicode(e)}')
try:
os.makedirs(pdir)
except OSError as e:
raise exc_class(f'Failed to make directory for userdb database at {pdir} with error: {as_unicode(e)}')
try:
return apsw.Connection(path)
except apsw.CantOpenError as e:
raise exc_class(f'Failed to open userdb database at {path} with error: {as_unicode(e)}')
class UserManager:
lock = RLock()
@property
def conn(self):
with self.lock:
if self._conn is None:
self._conn = connect(self.path)
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. (Technically, one can store
# a MD5 hash of the username+realm+password, but it has to be
# without salt so it is trivially brute-forceable, anyway)
# 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 "{}",
readonly TEXT NOT NULL DEFAULT "n",
misc_data TEXT NOT NULL DEFAULT "{}",
UNIQUE(name)
);
PRAGMA user_version=1;
''')
c.close()
return self._conn
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):
with self.lock:
for data, in self.conn.cursor().execute(
'SELECT session_data FROM users WHERE name=?', (username,)):
return load_json(data)
return {}
def set_session_data(self, username, data):
with self.lock:
conn = self.conn
c = conn.cursor()
data = as_json(data)
if isinstance(data, bytes):
data = data.decode('utf-8')
c.execute('UPDATE users SET session_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
def has_user(self, username):
return self.get(username) is not None
def validate_username(self, username):
if self.has_user(username):
return _('The username %s already exists') % username
return validate_username(username)
def validate_password(self, pw):
return validate_password(pw)
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 {}
self.conn.cursor().execute(
'INSERT INTO users (name, pw, restriction, readonly) VALUES (?, ?, ?, ?)',
(username, pw, serialize_restriction(restriction), ('y' if readonly else 'n')))
def remove_user(self, username):
with self.lock:
self.conn.cursor().execute('DELETE FROM users WHERE name=?', (username,))
return self.conn.changes() > 0
@property
def all_user_names(self):
with self.lock:
return {x for x, in self.conn.cursor().execute(
'SELECT name FROM users')}
@property
def user_data(self):
with self.lock:
ans = {}
for name, pw, restriction, readonly in self.conn.cursor().execute('SELECT name,pw,restriction,readonly FROM users'):
ans[name] = create_user_data(pw, readonly.lower() == 'y', restriction)
return ans
@user_data.setter
def user_data(self, users):
with self.lock, self.conn:
c = self.conn.cursor()
remove = self.all_user_names - set(users)
if remove:
c.executemany('DELETE FROM users WHERE name=?', [(n,) for n in remove])
for name, data in iteritems(users):
res = serialize_restriction(data['restriction'])
r = 'y' if data['readonly'] else 'n'
c.execute('UPDATE users SET pw=?, restriction=?, readonly=? WHERE name=?',
(data['pw'], res, r, name))
if self.conn.changes() > 0:
continue
c.execute('INSERT INTO USERS (name, pw, restriction, readonly) VALUES (?, ?, ?, ?)',
(name, data['pw'], res, r))
self.refresh()
def refresh(self):
pass # legacy compat
def is_readonly(self, username):
with self.lock:
for readonly, in self.conn.cursor().execute(
'SELECT readonly FROM users WHERE name=?', (username,)):
return readonly == 'y'
return False
def set_readonly(self, username, value):
with self.lock:
self.conn.cursor().execute(
'UPDATE users SET readonly=? WHERE name=?', ('y' if value else 'n', username))
def change_password(self, username, pw):
with self.lock:
msg = self.validate_password(pw)
if msg is not None:
raise ValueError(msg)
self.conn.cursor().execute(
'UPDATE users SET pw=? WHERE name=?', (pw, username))
def restrictions(self, username):
with self.lock:
for restriction, in self.conn.cursor().execute(
'SELECT restriction FROM users WHERE name=?', (username,)):
return parse_restriction(restriction).copy()
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.conn.cursor().execute(
'UPDATE users SET restriction=? WHERE name=?', (serialize_restriction(restrictions), username))
def library_restriction(self, username, library_path):
r = self.restrictions(username)
if r is None:
return ''
library_name = os.path.basename(library_path).lower()
return r['library_restrictions'].get(library_name) or ''