mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
CLI to manage user accounts for the new server
This commit is contained in:
parent
44ebdeb176
commit
145523e08a
@ -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 = []
|
||||
|
||||
|
@ -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,6 +74,100 @@ 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 '+ _(
|
||||
@ -98,11 +192,22 @@ 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)
|
||||
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):
|
||||
|
@ -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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user