CLI to manage user accounts for the new server

This commit is contained in:
Kovid Goyal 2015-11-05 18:45:25 +05:30
parent 44ebdeb176
commit 145523e08a
3 changed files with 186 additions and 34 deletions

View File

@ -110,6 +110,7 @@ raw_options = (
' will use "basic" if SSL is configured otherwise it will use "digest".',
)
assert len(raw_options) % 4 == 0
# TODO: Mark these strings for translation, once you finalize the option set
options = []

View File

@ -8,8 +8,8 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, signal
from calibre import as_unicode
from calibre.constants import plugins, iswindows
from calibre import as_unicode, prints
from calibre.constants import plugins, iswindows, preferred_encoding
from calibre.srv.errors import InvalidCredentials
from calibre.srv.loop import ServerLoop
from calibre.srv.bonjour import BonJour
@ -74,9 +74,103 @@ class Server(object):
from calibre.utils.rapydscript import compile_srv
compile_srv()
# Manage users CLI {{{
def manage_users(path=None):
from calibre.srv.users import UserManager
m = UserManager(path)
enc = getattr(sys.stdin, 'encoding', preferred_encoding) or preferred_encoding
def choice(question, choices, default=None, banner=''):
prints(banner)
for i, choice in enumerate(choices):
prints('%d)' % (i+1), choice)
print()
while True:
prompt = question + ' [1-%d]: ' % len(choices)
if default is not None:
prompt = question + ' [1-%d %s: %d]' % (len(choices), _('default'), default+1)
reply = raw_input(prompt)
if not reply and default is not None:
reply = str(default + 1)
if not reply:
raise SystemExit(0)
reply = reply.strip()
try:
num = int(reply) - 1
if not (0 <= num < len(choices)):
raise Exception('bad num')
return num
except Exception:
prints(_('%s is not a valid choice, try again') % reply)
def get_valid(prompt, invalidq=lambda x: None):
while True:
ans = raw_input(prompt + ': ').strip().decode(enc)
fail_message = invalidq(ans)
if fail_message is None:
return ans
prints(fail_message)
def get_valid_user():
prints(_('Existing user names:'))
users = sorted(m.all_user_names)
if not users:
raise SystemExit(_('There are no users, you must first add an user'))
prints(', '.join(users))
def validate(username):
if not m.has_user(username):
return _('The username %s does not exist') % username
return get_valid(_('Enter the username'), validate)
def get_pass(username):
while True:
from getpass import getpass
one = getpass(_('Enter the new password for %s: ') % username).decode(enc)
if not one:
prints(_('Empty passwords are not allowed'))
continue
two = getpass(_('Re-enter the new password for %s, to verify: ') % username).decode(enc)
if one != two:
prints(_('Passwords do not match'))
continue
msg = m.validate_password(one)
if msg is None:
return one
prints(msg)
def add_user():
username = get_valid(_('Enter the username'), m.validate_username)
pw = get_pass(username)
m.add_user(username, pw)
prints(_('User %s added successfully!') % username)
def remove_user():
un = get_valid_user()
if raw_input((_('Are you sure you want to remove the user %s?') % un) + ' [y/n]: ').decode(enc) != 'y':
raise SystemExit(0)
m.remove_user(un)
prints(_('User %s successfully removed!') % un)
def edit_user():
username = get_valid_user()
pw = get_pass(username)
m.change_password(username, pw)
prints(_('Password for %s successfully changed!') % username)
def show_password():
username = get_valid_user()
pw = m.get(username)
prints(_('Password for {0} is: {1}').format(username, pw))
{0:add_user, 1:edit_user, 2:remove_user, 3:show_password}[choice(_('What do you want to do?'), [
_('Add a new user'), _('Edit an existing user'), _('Remove a user'), _('Show the password for a user')])]()
# }}}
def create_option_parser():
parser = opts_to_parser('%prog '+ _(
parser=opts_to_parser('%prog '+ _(
'''[options] [path to library folder ...]
Start the calibre content server. The calibre content server
@ -98,19 +192,30 @@ program will be used.
help=_('Automatically reload server when source code changes. Useful'
' for development. You should also specify a small value for the'
' shutdown timeout.'))
parser.add_option(
'--manage-users', default=False, action='store_true',
help=_('Manage the database of users allowed to connect to this server.'
' See also the %s option.') % '--userdb')
return parser
def main(args=sys.argv):
opts, args = create_option_parser().parse_args(args)
libraries = args[1:]
opts, args=create_option_parser().parse_args(args)
if opts.manage_users:
try:
manage_users(opts.userdb)
except (KeyboardInterrupt, EOFError):
raise SystemExit(_('Interrupted by user'))
raise SystemExit(0)
libraries=args[1:]
for lib in libraries:
if not lib or not LibraryDatabase.exists_at(lib):
raise SystemExit(_('There is no calibre library at: %s') % lib)
if not libraries:
if not prefs['library_path']:
raise SystemExit(_('You must specify at least one calibre library'))
libraries = [prefs['library_path']]
libraries=[prefs['library_path']]
if opts.auto_reload:
if opts.daemonize:
@ -121,9 +226,9 @@ def main(args=sys.argv):
return auto_reload(default_log)
except NoAutoReload as e:
raise SystemExit(e.message)
opts.auto_reload_port = int(os.environ.get('CALIBRE_AUTORELOAD_PORT', 0))
opts.auto_reload_port=int(os.environ.get('CALIBRE_AUTORELOAD_PORT', 0))
try:
server = Server(libraries, opts)
server=Server(libraries, opts)
except InvalidCredentials as e:
raise SystemExit(e.message)
if opts.daemonize:

View File

@ -4,8 +4,8 @@
from __future__ import (unicode_literals, division, absolute_import,
print_function)
import os, json
from threading import Lock
import os, json, re
from threading import RLock
import apsw
@ -15,10 +15,11 @@ from calibre.utils.config import to_json, from_json
class UserManager(object):
lock = Lock()
lock = RLock()
@property
def conn(self):
with self.lock:
if self._conn is None:
self._conn = apsw.Connection(self.path)
with self._conn:
@ -36,6 +37,7 @@ class UserManager(object):
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)
);
@ -43,6 +45,7 @@ class UserManager(object):
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
@ -73,3 +76,46 @@ class UserManager(object):
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
if re.sub(r'[a-zA-Z0-9 ]', '', username):
return _('For maximum compatibility you should use only the letters A-Z, the numbers 0-9 and spaces in the username')
def validate_password(self, pw):
try:
pw = pw.encode('ascii', 'strict')
except ValueError:
return _('The password must contain only ASCII (English) characters and symbols')
def add_user(self, username, pw, restriction='', readonly=False):
with self.lock:
msg = self.validate_username(username) or self.validate_password(pw)
if msg is not None:
raise ValueError(msg)
self.conn.cursor().execute(
'INSERT INTO users (name, pw, restriction, readonly) VALUES (?, ?, ?, ?)',
(username, pw, 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')}
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))