mirror of
https://github.com/morpheus65535/bazarr.git
synced 2024-11-10 09:02:44 +08:00
Merge branch 'development' of http://github.com/morpheus65535/bazarr into development
This commit is contained in:
commit
da7c1aa5ad
15 changed files with 3667 additions and 1653 deletions
|
@ -60,7 +60,7 @@ def get_general_settings():
|
|||
if cfg.has_option('general', 'minimum_score'):
|
||||
minimum_score = cfg.get('general', 'minimum_score')
|
||||
else:
|
||||
minimum_score = '100'
|
||||
minimum_score = '90'
|
||||
|
||||
if cfg.has_option('general', 'use_scenename'):
|
||||
use_scenename = cfg.getboolean('general', 'use_scenename')
|
||||
|
@ -130,7 +130,7 @@ def get_general_settings():
|
|||
if cfg.has_option('general', 'minimum_score_movie'):
|
||||
minimum_score_movie = cfg.get('general', 'minimum_score_movie')
|
||||
else:
|
||||
minimum_score_movie = '100'
|
||||
minimum_score_movie = '70'
|
||||
|
||||
if cfg.has_option('general', 'use_embedded_subs'):
|
||||
use_embedded_subs = cfg.getboolean('general', 'use_embedded_subs')
|
||||
|
@ -156,7 +156,7 @@ def get_general_settings():
|
|||
branch = 'master'
|
||||
auto_update = True
|
||||
single_language = False
|
||||
minimum_score = '100'
|
||||
minimum_score = '90'
|
||||
use_scenename = False
|
||||
use_postprocessing = False
|
||||
postprocessing_cmd = False
|
||||
|
@ -170,7 +170,7 @@ def get_general_settings():
|
|||
movie_default_language = []
|
||||
movie_default_hi = False
|
||||
page_size = '25'
|
||||
minimum_score_movie = '100'
|
||||
minimum_score_movie = '70'
|
||||
use_embedded_subs = False
|
||||
only_monitored = False
|
||||
adaptive_searching = False
|
||||
|
@ -187,10 +187,10 @@ def get_auth_settings():
|
|||
pass
|
||||
|
||||
if cfg.has_section('auth'):
|
||||
if cfg.has_option('auth', 'enabled'):
|
||||
enabled = cfg.getboolean('auth', 'enabled')
|
||||
if cfg.has_option('auth', 'type'):
|
||||
type = cfg.get('auth', 'type')
|
||||
else:
|
||||
enabled = False
|
||||
type = None
|
||||
|
||||
if cfg.has_option('auth', 'username'):
|
||||
username = cfg.get('auth', 'username')
|
||||
|
@ -202,11 +202,11 @@ def get_auth_settings():
|
|||
else:
|
||||
password = ''
|
||||
else:
|
||||
enabled = False
|
||||
type = None
|
||||
username = ''
|
||||
password = ''
|
||||
|
||||
return [enabled, username, password]
|
||||
return [type, username, password]
|
||||
|
||||
|
||||
def get_sonarr_settings():
|
||||
|
|
35
init.py
35
init.py
|
@ -27,7 +27,7 @@ if os.path.exists(os.path.join(config_dir, 'log')) is False:
|
|||
|
||||
config_file = os.path.normpath(os.path.join(config_dir, 'config/config.ini'))
|
||||
|
||||
# if os.path.exists(os.path.join(config_dir, 'db/bazarr.db')) is True and os.path.exists(config_file) is False:
|
||||
cfg = ConfigParser()
|
||||
try:
|
||||
# Open database connection
|
||||
db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'data/db/bazarr.db'), timeout=30)
|
||||
|
@ -51,8 +51,6 @@ try:
|
|||
# Close database connection
|
||||
db.close()
|
||||
|
||||
cfg = ConfigParser()
|
||||
|
||||
section = 'general'
|
||||
|
||||
if not cfg.has_section(section):
|
||||
|
@ -164,7 +162,7 @@ except sqlite3.OperationalError:
|
|||
if not cfg.has_section(section):
|
||||
cfg.add_section(section)
|
||||
|
||||
cfg.set(section, 'enabled', "False")
|
||||
cfg.set(section, 'type', "None")
|
||||
cfg.set(section, 'username', "")
|
||||
cfg.set(section, 'password', "")
|
||||
|
||||
|
@ -218,3 +216,32 @@ try:
|
|||
logging.info('Database created successfully')
|
||||
except:
|
||||
pass
|
||||
|
||||
# Remove unused settings
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
cfg.read_file(f)
|
||||
except Exception:
|
||||
pass
|
||||
cfg.remove_option('auth', 'enabled')
|
||||
with open(config_file, 'w+') as configfile:
|
||||
cfg.write(configfile)
|
||||
|
||||
from cork import Cork
|
||||
import time
|
||||
if os.path.exists(os.path.normpath(os.path.join(config_dir, 'config/users.json'))) is False:
|
||||
cork = Cork(os.path.normpath(os.path.join(config_dir, 'config')), initialize=True)
|
||||
|
||||
cork._store.roles[''] = 100
|
||||
cork._store.save_roles()
|
||||
|
||||
tstamp = str(time.time())
|
||||
username = password = ''
|
||||
cork._store.users[username] = {
|
||||
'role': '',
|
||||
'hash': cork._hash(username, password),
|
||||
'email_addr': username,
|
||||
'desc': username,
|
||||
'creation_date': tstamp
|
||||
}
|
||||
cork._store.save_users()
|
||||
|
|
7
libs/cork/__init__.py
Normal file
7
libs/cork/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
#
|
||||
# Backends API - used to make backends available for importing
|
||||
#
|
||||
from .cork import Cork, JsonBackend, AAAException, AuthException, Mailer, FlaskCork, Redirect
|
13
libs/cork/backends.py
Normal file
13
libs/cork/backends.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
|
||||
"""
|
||||
.. module:: backends
|
||||
:synopsis: Backends API - used to make backends available for importing
|
||||
"""
|
||||
|
||||
from .json_backend import JsonBackend
|
||||
from .mongodb_backend import MongoDBBackend
|
||||
from .sqlalchemy_backend import SqlAlchemyBackend
|
||||
from .sqlite_backend import SQLiteBackend
|
31
libs/cork/base_backend.py
Normal file
31
libs/cork/base_backend.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
|
||||
"""
|
||||
.. module:: backend.py
|
||||
:synopsis: Base Backend.
|
||||
"""
|
||||
|
||||
class BackendIOException(Exception):
|
||||
"""Generic Backend I/O Exception"""
|
||||
pass
|
||||
|
||||
def ni(*args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
class Backend(object):
|
||||
"""Base Backend class - to be subclassed by real backends."""
|
||||
save_users = ni
|
||||
save_roles = ni
|
||||
save_pending_registrations = ni
|
||||
|
||||
class Table(object):
|
||||
"""Base Table class - to be subclassed by real backends."""
|
||||
__len__ = ni
|
||||
__contains__ = ni
|
||||
__setitem__ = ni
|
||||
__getitem__ = ni
|
||||
__iter__ = ni
|
||||
iteritems = ni
|
||||
|
975
libs/cork/cork.py
Normal file
975
libs/cork/cork.py
Normal file
|
@ -0,0 +1,975 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
#
|
||||
# This package is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 3 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This package is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from base64 import b64encode, b64decode
|
||||
from datetime import datetime, timedelta
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from logging import getLogger
|
||||
from smtplib import SMTP, SMTP_SSL
|
||||
from threading import Thread
|
||||
from time import time
|
||||
import bottle
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import scrypt
|
||||
scrypt_available = True
|
||||
except ImportError: # pragma: no cover
|
||||
scrypt_available = False
|
||||
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
from .backends import JsonBackend
|
||||
|
||||
is_py3 = (sys.version_info.major == 3)
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
class AAAException(Exception):
|
||||
"""Generic Authentication/Authorization Exception"""
|
||||
pass
|
||||
|
||||
|
||||
class AuthException(AAAException):
|
||||
"""Authentication Exception: incorrect username/password pair"""
|
||||
pass
|
||||
|
||||
|
||||
class BaseCork(object):
|
||||
"""Abstract class"""
|
||||
|
||||
def __init__(self, directory=None, backend=None, email_sender=None,
|
||||
initialize=False, session_domain=None, smtp_server=None,
|
||||
smtp_url='localhost', session_key_name=None):
|
||||
"""Auth/Authorization/Accounting class
|
||||
|
||||
:param directory: configuration directory
|
||||
:type directory: str.
|
||||
:param users_fname: users filename (without .json), defaults to 'users'
|
||||
:type users_fname: str.
|
||||
:param roles_fname: roles filename (without .json), defaults to 'roles'
|
||||
:type roles_fname: str.
|
||||
"""
|
||||
if smtp_server:
|
||||
smtp_url = smtp_server
|
||||
self.mailer = Mailer(email_sender, smtp_url)
|
||||
self.password_reset_timeout = 3600 * 24
|
||||
self.session_domain = session_domain
|
||||
self.session_key_name = session_key_name or 'beaker.session'
|
||||
self.preferred_hashing_algorithm = 'PBKDF2'
|
||||
|
||||
# Setup JsonBackend by default for backward compatibility.
|
||||
if backend is None:
|
||||
self._store = JsonBackend(directory, users_fname='users',
|
||||
roles_fname='roles', pending_reg_fname='register',
|
||||
initialize=initialize)
|
||||
|
||||
else:
|
||||
self._store = backend
|
||||
|
||||
def login(self, username, password, success_redirect=None,
|
||||
fail_redirect=None):
|
||||
"""Check login credentials for an existing user.
|
||||
Optionally redirect the user to another page (typically /login)
|
||||
|
||||
:param username: username
|
||||
:type username: str or unicode.
|
||||
:param password: cleartext password
|
||||
:type password: str.or unicode
|
||||
:param success_redirect: redirect authorized users (optional)
|
||||
:type success_redirect: str.
|
||||
:param fail_redirect: redirect unauthorized users (optional)
|
||||
:type fail_redirect: str.
|
||||
:returns: True for successful logins, else False
|
||||
"""
|
||||
#assert isinstance(username, type(u'')), "the username must be a string"
|
||||
#assert isinstance(password, type(u'')), "the password must be a string"
|
||||
|
||||
if username in self._store.users:
|
||||
salted_hash = self._store.users[username]['hash']
|
||||
if hasattr(salted_hash, 'encode'):
|
||||
salted_hash = salted_hash.encode('ascii')
|
||||
authenticated = self._verify_password(
|
||||
username,
|
||||
password,
|
||||
salted_hash,
|
||||
)
|
||||
if authenticated:
|
||||
# Setup session data
|
||||
self._setup_cookie(username)
|
||||
self._store.users[username]['last_login'] = str(datetime.utcnow())
|
||||
self._store.save_users()
|
||||
if success_redirect:
|
||||
self._redirect(success_redirect)
|
||||
return True
|
||||
|
||||
if fail_redirect:
|
||||
self._redirect(fail_redirect)
|
||||
|
||||
return False
|
||||
|
||||
def logout(self, success_redirect='/login', fail_redirect='/login'):
|
||||
"""Log the user out, remove cookie
|
||||
|
||||
:param success_redirect: redirect the user after logging out
|
||||
:type success_redirect: str.
|
||||
:param fail_redirect: redirect the user if it is not logged in
|
||||
:type fail_redirect: str.
|
||||
"""
|
||||
try:
|
||||
session = self._beaker_session
|
||||
session.delete()
|
||||
except Exception as e:
|
||||
log.debug("Exception %s while logging out." % repr(e))
|
||||
self._redirect(fail_redirect)
|
||||
|
||||
self._redirect(success_redirect)
|
||||
|
||||
def require(self, username=None, role=None, fixed_role=False,
|
||||
fail_redirect=None):
|
||||
"""Ensure the user is logged in has the required role (or higher).
|
||||
Optionally redirect the user to another page (typically /login)
|
||||
If both `username` and `role` are specified, both conditions need to be
|
||||
satisfied.
|
||||
If none is specified, any authenticated user will be authorized.
|
||||
By default, any role with higher level than `role` will be authorized;
|
||||
set fixed_role=True to prevent this.
|
||||
|
||||
:param username: username (optional)
|
||||
:type username: str.
|
||||
:param role: role
|
||||
:type role: str.
|
||||
:param fixed_role: require user role to match `role` strictly
|
||||
:type fixed_role: bool.
|
||||
:param redirect: redirect unauthorized users (optional)
|
||||
:type redirect: str.
|
||||
"""
|
||||
# Parameter validation
|
||||
if username is not None:
|
||||
if username not in self._store.users:
|
||||
raise AAAException("Nonexistent user")
|
||||
|
||||
if fixed_role and role is None:
|
||||
raise AAAException(
|
||||
"""A role must be specified if fixed_role has been set""")
|
||||
|
||||
if role is not None and role not in self._store.roles:
|
||||
raise AAAException("Role not found")
|
||||
|
||||
# Authentication
|
||||
try:
|
||||
cu = self.current_user
|
||||
except AAAException:
|
||||
if fail_redirect is None:
|
||||
raise AuthException("Unauthenticated user")
|
||||
else:
|
||||
self._redirect(fail_redirect)
|
||||
|
||||
# Authorization
|
||||
if cu.role not in self._store.roles:
|
||||
raise AAAException("Role not found for the current user")
|
||||
|
||||
if username is not None:
|
||||
# A specific user is required
|
||||
if username == self.current_user.username:
|
||||
return
|
||||
|
||||
if fail_redirect is None:
|
||||
raise AuthException("Unauthorized access: incorrect"
|
||||
" username")
|
||||
|
||||
self._redirect(fail_redirect)
|
||||
|
||||
if fixed_role:
|
||||
# A specific role is required
|
||||
if role == self.current_user.role:
|
||||
return
|
||||
|
||||
if fail_redirect is None:
|
||||
raise AuthException("Unauthorized access: incorrect role")
|
||||
|
||||
self._redirect(fail_redirect)
|
||||
|
||||
if role is not None:
|
||||
# Any role with higher level is allowed
|
||||
current_lvl = self._store.roles[self.current_user.role]
|
||||
threshold_lvl = self._store.roles[role]
|
||||
if current_lvl >= threshold_lvl:
|
||||
return
|
||||
|
||||
if fail_redirect is None:
|
||||
raise AuthException("Unauthorized access: ")
|
||||
|
||||
self._redirect(fail_redirect)
|
||||
|
||||
return # success
|
||||
|
||||
def create_role(self, role, level):
|
||||
"""Create a new role.
|
||||
|
||||
:param role: role name
|
||||
:type role: str.
|
||||
:param level: role level (0=lowest, 100=admin)
|
||||
:type level: int.
|
||||
:raises: AuthException on errors
|
||||
"""
|
||||
if self.current_user.level < 100:
|
||||
raise AuthException("The current user is not authorized to ")
|
||||
if role in self._store.roles:
|
||||
raise AAAException("The role is already existing")
|
||||
try:
|
||||
int(level)
|
||||
except ValueError:
|
||||
raise AAAException("The level must be numeric.")
|
||||
self._store.roles[role] = level
|
||||
self._store.save_roles()
|
||||
|
||||
def delete_role(self, role):
|
||||
"""Deleta a role.
|
||||
|
||||
:param role: role name
|
||||
:type role: str.
|
||||
:raises: AuthException on errors
|
||||
"""
|
||||
if self.current_user.level < 100:
|
||||
raise AuthException("The current user is not authorized to ")
|
||||
if role not in self._store.roles:
|
||||
raise AAAException("Nonexistent role.")
|
||||
self._store.roles.pop(role)
|
||||
self._store.save_roles()
|
||||
|
||||
def list_roles(self):
|
||||
"""List roles.
|
||||
|
||||
:returns: (role, role_level) generator (sorted by role)
|
||||
"""
|
||||
for role in sorted(self._store.roles):
|
||||
yield (role, self._store.roles[role])
|
||||
|
||||
def create_user(self, username, role, password, email_addr=None,
|
||||
description=None):
|
||||
"""Create a new user account.
|
||||
This method is available to users with level>=100
|
||||
|
||||
:param username: username
|
||||
:type username: str.
|
||||
:param role: role
|
||||
:type role: str.
|
||||
:param password: cleartext password
|
||||
:type password: str.
|
||||
:param email_addr: email address (optional)
|
||||
:type email_addr: str.
|
||||
:param description: description (free form)
|
||||
:type description: str.
|
||||
:raises: AuthException on errors
|
||||
"""
|
||||
assert username, "Username must be provided."
|
||||
if self.current_user.level < 100:
|
||||
raise AuthException("The current user is not authorized"
|
||||
" to create users.")
|
||||
|
||||
if username in self._store.users:
|
||||
raise AAAException("User is already existing.")
|
||||
if role not in self._store.roles:
|
||||
raise AAAException("Nonexistent user role.")
|
||||
tstamp = str(datetime.utcnow())
|
||||
h = self._hash(username, password)
|
||||
h = h.decode('ascii')
|
||||
self._store.users[username] = {
|
||||
'role': role,
|
||||
'hash': h,
|
||||
'email_addr': email_addr,
|
||||
'desc': description,
|
||||
'creation_date': tstamp,
|
||||
'last_login': tstamp
|
||||
}
|
||||
self._store.save_users()
|
||||
|
||||
def delete_user(self, username):
|
||||
"""Delete a user account.
|
||||
This method is available to users with level>=100
|
||||
|
||||
:param username: username
|
||||
:type username: str.
|
||||
:raises: Exceptions on errors
|
||||
"""
|
||||
if self.current_user.level < 100:
|
||||
raise AuthException("The current user is not authorized to ")
|
||||
if username not in self._store.users:
|
||||
raise AAAException("Nonexistent user.")
|
||||
self.user(username).delete()
|
||||
|
||||
def list_users(self):
|
||||
"""List users.
|
||||
|
||||
:return: (username, role, email_addr, description) generator (sorted by
|
||||
username)
|
||||
"""
|
||||
for un in sorted(self._store.users):
|
||||
d = self._store.users[un]
|
||||
yield (un, d['role'], d['email_addr'], d['desc'])
|
||||
|
||||
@property
|
||||
def current_user(self):
|
||||
"""Current autenticated user
|
||||
|
||||
:returns: User() instance, if authenticated
|
||||
:raises: AuthException otherwise
|
||||
"""
|
||||
session = self._beaker_session
|
||||
username = session.get('username', None)
|
||||
if username is None:
|
||||
raise AuthException("Unauthenticated user")
|
||||
if username is not None and username in self._store.users:
|
||||
return User(username, self, session=session)
|
||||
raise AuthException("Unknown user: %s" % username)
|
||||
|
||||
@property
|
||||
def user_is_anonymous(self):
|
||||
"""Check if the current user is anonymous.
|
||||
|
||||
:returns: True if the user is anonymous, False otherwise
|
||||
:raises: AuthException if the session username is unknown
|
||||
"""
|
||||
try:
|
||||
username = self._beaker_session['username']
|
||||
except KeyError:
|
||||
return True
|
||||
|
||||
if username not in self._store.users:
|
||||
raise AuthException("Unknown user: %s" % username)
|
||||
|
||||
return False
|
||||
|
||||
def user(self, username):
|
||||
"""Existing user
|
||||
|
||||
:returns: User() instance if the user exist, None otherwise
|
||||
"""
|
||||
if username is not None and username in self._store.users:
|
||||
return User(username, self)
|
||||
return None
|
||||
|
||||
def register(self, username, password, email_addr, role='user',
|
||||
max_level=50, subject="Signup confirmation",
|
||||
email_template='views/registration_email.tpl',
|
||||
description=None, **kwargs):
|
||||
"""Register a new user account. An email with a registration validation
|
||||
is sent to the user.
|
||||
WARNING: this method is available to unauthenticated users
|
||||
|
||||
:param username: username
|
||||
:type username: str.
|
||||
:param password: cleartext password
|
||||
:type password: str.
|
||||
:param role: role (optional), defaults to 'user'
|
||||
:type role: str.
|
||||
:param max_level: maximum role level (optional), defaults to 50
|
||||
:type max_level: int.
|
||||
:param email_addr: email address
|
||||
:type email_addr: str.
|
||||
:param subject: email subject
|
||||
:type subject: str.
|
||||
:param email_template: email template filename
|
||||
:type email_template: str.
|
||||
:param description: description (free form)
|
||||
:type description: str.
|
||||
:raises: AssertError or AAAException on errors
|
||||
"""
|
||||
assert username, "Username must be provided."
|
||||
assert password, "A password must be provided."
|
||||
assert email_addr, "An email address must be provided."
|
||||
if username in self._store.users:
|
||||
raise AAAException("User is already existing.")
|
||||
if role not in self._store.roles:
|
||||
raise AAAException("Nonexistent role")
|
||||
if self._store.roles[role] > max_level:
|
||||
raise AAAException("Unauthorized role")
|
||||
|
||||
registration_code = uuid.uuid4().hex
|
||||
creation_date = str(datetime.utcnow())
|
||||
|
||||
# send registration email
|
||||
email_text = bottle.template(
|
||||
email_template,
|
||||
username=username,
|
||||
email_addr=email_addr,
|
||||
role=role,
|
||||
creation_date=creation_date,
|
||||
registration_code=registration_code,
|
||||
**kwargs
|
||||
)
|
||||
self.mailer.send_email(email_addr, subject, email_text)
|
||||
|
||||
# store pending registration
|
||||
h = self._hash(username, password)
|
||||
h = h.decode('ascii')
|
||||
self._store.pending_registrations[registration_code] = {
|
||||
'username': username,
|
||||
'role': role,
|
||||
'hash': h,
|
||||
'email_addr': email_addr,
|
||||
'desc': description,
|
||||
'creation_date': creation_date,
|
||||
}
|
||||
self._store.save_pending_registrations()
|
||||
|
||||
def validate_registration(self, registration_code):
|
||||
"""Validate pending account registration, create a new account if
|
||||
successful.
|
||||
|
||||
:param registration_code: registration code
|
||||
:type registration_code: str.
|
||||
"""
|
||||
try:
|
||||
data = self._store.pending_registrations.pop(registration_code)
|
||||
except KeyError:
|
||||
raise AuthException("Invalid registration code.")
|
||||
|
||||
username = data['username']
|
||||
if username in self._store.users:
|
||||
raise AAAException("User is already existing.")
|
||||
|
||||
# the user data is moved from pending_registrations to _users
|
||||
self._store.users[username] = {
|
||||
'role': data['role'],
|
||||
'hash': data['hash'],
|
||||
'email_addr': data['email_addr'],
|
||||
'desc': data['desc'],
|
||||
'creation_date': data['creation_date'],
|
||||
'last_login': str(datetime.utcnow())
|
||||
}
|
||||
self._store.save_users()
|
||||
|
||||
def send_password_reset_email(self, username=None, email_addr=None,
|
||||
subject="Password reset confirmation",
|
||||
email_template='views/password_reset_email',
|
||||
**kwargs):
|
||||
"""Email the user with a link to reset his/her password
|
||||
If only one parameter is passed, fetch the other from the users
|
||||
database. If both are passed they will be matched against the users
|
||||
database as a security check.
|
||||
|
||||
:param username: username
|
||||
:type username: str.
|
||||
:param email_addr: email address
|
||||
:type email_addr: str.
|
||||
:param subject: email subject
|
||||
:type subject: str.
|
||||
:param email_template: email template filename
|
||||
:type email_template: str.
|
||||
:raises: AAAException on missing username or email_addr,
|
||||
AuthException on incorrect username/email_addr pair
|
||||
"""
|
||||
if username is None:
|
||||
if email_addr is None:
|
||||
raise AAAException("At least `username` or `email_addr` must"
|
||||
" be specified.")
|
||||
|
||||
# only email_addr is specified: fetch the username
|
||||
for k, v in self._store.users.iteritems():
|
||||
if v['email_addr'] == email_addr:
|
||||
username = k
|
||||
break
|
||||
else:
|
||||
raise AAAException("Email address not found.")
|
||||
|
||||
else: # username is provided
|
||||
if username not in self._store.users:
|
||||
raise AAAException("Nonexistent user.")
|
||||
if email_addr is None:
|
||||
email_addr = self._store.users[username].get('email_addr', None)
|
||||
if not email_addr:
|
||||
raise AAAException("Email address not available.")
|
||||
else:
|
||||
# both username and email_addr are provided: check them
|
||||
stored_email_addr = self._store.users[username]['email_addr']
|
||||
if email_addr != stored_email_addr:
|
||||
raise AuthException("Username/email address pair not found.")
|
||||
|
||||
# generate a reset_code token
|
||||
reset_code = self._reset_code(username, email_addr)
|
||||
|
||||
# send reset email
|
||||
email_text = bottle.template(
|
||||
email_template,
|
||||
username=username,
|
||||
email_addr=email_addr,
|
||||
reset_code=reset_code,
|
||||
**kwargs
|
||||
)
|
||||
self.mailer.send_email(email_addr, subject, email_text)
|
||||
|
||||
def reset_password(self, reset_code, password):
|
||||
"""Validate reset_code and update the account password
|
||||
The username is extracted from the reset_code token
|
||||
|
||||
:param reset_code: reset token
|
||||
:type reset_code: str.
|
||||
:param password: new password
|
||||
:type password: str.
|
||||
:raises: AuthException for invalid reset tokens, AAAException
|
||||
"""
|
||||
try:
|
||||
reset_code = b64decode(reset_code).decode()
|
||||
username, email_addr, tstamp, h = reset_code.split(':', 3)
|
||||
tstamp = int(tstamp)
|
||||
assert isinstance(username, type(u''))
|
||||
assert isinstance(email_addr, type(u''))
|
||||
if not isinstance(h, type(b'')):
|
||||
h = h.encode('utf-8')
|
||||
except (TypeError, ValueError):
|
||||
raise AuthException("Invalid reset code.")
|
||||
|
||||
if time() - tstamp > self.password_reset_timeout:
|
||||
raise AuthException("Expired reset code.")
|
||||
|
||||
assert isinstance(h, type(b''))
|
||||
if not self._verify_password(username, email_addr, h):
|
||||
raise AuthException("Invalid reset code.")
|
||||
user = self.user(username)
|
||||
if user is None:
|
||||
raise AAAException("Nonexistent user.")
|
||||
user.update(pwd=password)
|
||||
|
||||
def make_auth_decorator(self, username=None, role=None, fixed_role=False, fail_redirect='/login'):
|
||||
'''
|
||||
Create a decorator to be used for authentication and authorization
|
||||
|
||||
:param username: A resource can be protected for a specific user
|
||||
:param role: Minimum role level required for authorization
|
||||
:param fixed_role: Only this role gets authorized
|
||||
:param fail_redirect: The URL to redirect to if a login is required.
|
||||
'''
|
||||
session_manager = self
|
||||
def auth_require(username=username, role=role, fixed_role=fixed_role,
|
||||
fail_redirect=fail_redirect):
|
||||
def decorator(func):
|
||||
import functools
|
||||
@functools.wraps(func)
|
||||
def wrapper(*a, **ka):
|
||||
session_manager.require(username=username, role=role, fixed_role=fixed_role,
|
||||
fail_redirect=fail_redirect)
|
||||
return func(*a, **ka)
|
||||
return wrapper
|
||||
return decorator
|
||||
return(auth_require)
|
||||
|
||||
|
||||
## Private methods
|
||||
|
||||
def _setup_cookie(self, username):
|
||||
"""Setup cookie for a user that just logged in"""
|
||||
session = self._beaker_session
|
||||
session['username'] = username
|
||||
if self.session_domain is not None:
|
||||
session.domain = self.session_domain
|
||||
|
||||
self._save_session()
|
||||
|
||||
def _hash(self, username, pwd, salt=None, algo=None):
|
||||
"""Hash username and password, generating salt value if required
|
||||
"""
|
||||
if algo is None:
|
||||
algo = self.preferred_hashing_algorithm
|
||||
|
||||
if algo == 'PBKDF2':
|
||||
return self._hash_pbkdf2(username, pwd, salt=salt)
|
||||
|
||||
if algo == 'scrypt':
|
||||
return self._hash_scrypt(username, pwd, salt=salt)
|
||||
|
||||
raise RuntimeError("Unknown hashing algorithm requested: %s" % algo)
|
||||
|
||||
@staticmethod
|
||||
def _hash_scrypt(username, pwd, salt=None):
|
||||
"""Hash username and password, generating salt value if required
|
||||
Use scrypt.
|
||||
|
||||
:returns: base-64 encoded str.
|
||||
"""
|
||||
if not scrypt_available:
|
||||
raise Exception("scrypt.hash required."
|
||||
" Please install the scrypt library.")
|
||||
|
||||
if salt is None:
|
||||
salt = os.urandom(32)
|
||||
|
||||
assert len(salt) == 32, "Incorrect salt length"
|
||||
|
||||
cleartext = "%s\0%s" % (username, pwd)
|
||||
h = scrypt.hash(cleartext, salt)
|
||||
|
||||
# 's' for scrypt
|
||||
hashed = b's' + salt + h
|
||||
return b64encode(hashed)
|
||||
|
||||
@staticmethod
|
||||
def _hash_pbkdf2(username, pwd, salt=None):
|
||||
"""Hash username and password, generating salt value if required
|
||||
Use PBKDF2 from Beaker
|
||||
|
||||
:returns: base-64 encoded str.
|
||||
"""
|
||||
if salt is None:
|
||||
salt = os.urandom(32)
|
||||
|
||||
assert isinstance(salt, bytes)
|
||||
assert len(salt) == 32, "Incorrect salt length"
|
||||
|
||||
username = username.encode('utf-8')
|
||||
assert isinstance(username, bytes)
|
||||
|
||||
pwd = pwd.encode('utf-8')
|
||||
assert isinstance(pwd, bytes)
|
||||
|
||||
cleartext = username + b'\0' + pwd
|
||||
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
|
||||
|
||||
# 'p' for PBKDF2
|
||||
hashed = b'p' + salt + h
|
||||
return b64encode(hashed)
|
||||
|
||||
def _verify_password(self, username, pwd, salted_hash):
|
||||
"""Verity username/password pair against a salted hash
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
assert isinstance(salted_hash, type(b''))
|
||||
decoded = b64decode(salted_hash)
|
||||
hash_type = decoded[0]
|
||||
if isinstance(hash_type, int):
|
||||
hash_type = chr(hash_type)
|
||||
|
||||
salt = decoded[1:33]
|
||||
|
||||
if hash_type == 'p': # PBKDF2
|
||||
h = self._hash_pbkdf2(username, pwd, salt)
|
||||
return salted_hash == h
|
||||
|
||||
if hash_type == 's': # scrypt
|
||||
h = self._hash_scrypt(username, pwd, salt)
|
||||
return salted_hash == h
|
||||
|
||||
raise RuntimeError("Unknown hashing algorithm in hash: %r" % decoded)
|
||||
|
||||
def _purge_expired_registrations(self, exp_time=96):
|
||||
"""Purge expired registration requests.
|
||||
|
||||
:param exp_time: expiration time (hours)
|
||||
:type exp_time: float.
|
||||
"""
|
||||
pending = self._store.pending_registrations.items()
|
||||
if is_py3:
|
||||
pending = list(pending)
|
||||
|
||||
for uuid_code, data in pending:
|
||||
creation = datetime.strptime(data['creation_date'],
|
||||
"%Y-%m-%d %H:%M:%S.%f")
|
||||
now = datetime.utcnow()
|
||||
maxdelta = timedelta(hours=exp_time)
|
||||
if now - creation > maxdelta:
|
||||
self._store.pending_registrations.pop(uuid_code)
|
||||
|
||||
def _reset_code(self, username, email_addr):
|
||||
"""generate a reset_code token
|
||||
|
||||
:param username: username
|
||||
:type username: str.
|
||||
:param email_addr: email address
|
||||
:type email_addr: str.
|
||||
:returns: Base-64 encoded token
|
||||
"""
|
||||
h = self._hash(username, email_addr)
|
||||
t = "%d" % time()
|
||||
t = t.encode('utf-8')
|
||||
reset_code = b':'.join((username.encode('utf-8'), email_addr.encode('utf-8'), t, h))
|
||||
return b64encode(reset_code)
|
||||
|
||||
|
||||
class User(object):
|
||||
|
||||
def __init__(self, username, cork_obj, session=None):
|
||||
"""Represent an authenticated user, exposing useful attributes:
|
||||
username, role, level, description, email_addr, session_creation_time,
|
||||
session_accessed_time, session_id. The session-related attributes are
|
||||
available for the current user only.
|
||||
|
||||
:param username: username
|
||||
:type username: str.
|
||||
:param cork_obj: instance of :class:`Cork`
|
||||
"""
|
||||
self._cork = cork_obj
|
||||
assert username in self._cork._store.users, "Unknown user"
|
||||
self.username = username
|
||||
user_data = self._cork._store.users[username]
|
||||
self.role = user_data['role']
|
||||
self.description = user_data['desc']
|
||||
self.email_addr = user_data['email_addr']
|
||||
self.level = self._cork._store.roles[self.role]
|
||||
|
||||
if session is not None:
|
||||
try:
|
||||
self.session_creation_time = session['_creation_time']
|
||||
self.session_accessed_time = session['_accessed_time']
|
||||
self.session_id = session['_id']
|
||||
except:
|
||||
pass
|
||||
|
||||
def update(self, role=None, pwd=None, email_addr=None):
|
||||
"""Update an user account data
|
||||
|
||||
:param role: change user role, if specified
|
||||
:type role: str.
|
||||
:param pwd: change user password, if specified
|
||||
:type pwd: str.
|
||||
:param email_addr: change user email address, if specified
|
||||
:type email_addr: str.
|
||||
:raises: AAAException on nonexistent user or role.
|
||||
"""
|
||||
username = self.username
|
||||
if username not in self._cork._store.users:
|
||||
raise AAAException("User does not exist.")
|
||||
|
||||
if role is not None:
|
||||
if role not in self._cork._store.roles:
|
||||
raise AAAException("Nonexistent role.")
|
||||
|
||||
self._cork._store.users[username]['role'] = role
|
||||
|
||||
if pwd is not None:
|
||||
self._cork._store.users[username]['hash'] = self._cork._hash(
|
||||
username, pwd)
|
||||
|
||||
if email_addr is not None:
|
||||
self._cork._store.users[username]['email_addr'] = email_addr
|
||||
|
||||
self._cork._store.save_users()
|
||||
|
||||
def delete(self):
|
||||
"""Delete user account
|
||||
|
||||
:raises: AAAException on nonexistent user.
|
||||
"""
|
||||
try:
|
||||
self._cork._store.users.pop(self.username)
|
||||
except KeyError:
|
||||
raise AAAException("Nonexistent user.")
|
||||
self._cork._store.save_users()
|
||||
|
||||
|
||||
class Redirect(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def raise_redirect(path):
|
||||
raise Redirect(path)
|
||||
|
||||
|
||||
class Cork(BaseCork):
|
||||
@staticmethod
|
||||
def _redirect(location):
|
||||
bottle.redirect(location)
|
||||
|
||||
@property
|
||||
def _beaker_session(self):
|
||||
"""Get session"""
|
||||
return bottle.request.environ.get(self.session_key_name)
|
||||
|
||||
def _save_session(self):
|
||||
self._beaker_session.save()
|
||||
|
||||
|
||||
class FlaskCork(BaseCork):
|
||||
@staticmethod
|
||||
def _redirect(location):
|
||||
raise_redirect(location)
|
||||
|
||||
@property
|
||||
def _beaker_session(self):
|
||||
"""Get session"""
|
||||
import flask
|
||||
return flask.session
|
||||
|
||||
def _save_session(self):
|
||||
pass
|
||||
|
||||
|
||||
class Mailer(object):
|
||||
|
||||
def __init__(self, sender, smtp_url, join_timeout=5, use_threads=True):
|
||||
"""Send emails asyncronously
|
||||
|
||||
:param sender: Sender email address
|
||||
:type sender: str.
|
||||
:param smtp_server: SMTP server
|
||||
:type smtp_server: str.
|
||||
"""
|
||||
self.sender = sender
|
||||
self.join_timeout = join_timeout
|
||||
self.use_threads = use_threads
|
||||
self._threads = []
|
||||
self._conf = self._parse_smtp_url(smtp_url)
|
||||
|
||||
def _parse_smtp_url(self, url):
|
||||
"""Parse SMTP URL"""
|
||||
match = re.match(r"""
|
||||
( # Optional protocol
|
||||
(?P<proto>smtp|starttls|ssl) # Protocol name
|
||||
://
|
||||
)?
|
||||
( # Optional user:pass@
|
||||
(?P<user>[^:]*) # Match every char except ':'
|
||||
(: (?P<pass>.*) )? @ # Optional :pass
|
||||
)?
|
||||
(?P<fqdn> # Required FQDN on IP address
|
||||
()| # Empty string
|
||||
( # FQDN
|
||||
[a-zA-Z_\-] # First character cannot be a number
|
||||
[a-zA-Z0-9_\-\.]{,254}
|
||||
)
|
||||
|( # IPv4
|
||||
([0-9]{1,3}\.){3}
|
||||
[0-9]{1,3}
|
||||
)
|
||||
|( # IPv6
|
||||
\[ # Square brackets
|
||||
([0-9a-f]{,4}:){1,8}
|
||||
[0-9a-f]{,4}
|
||||
\]
|
||||
)
|
||||
)
|
||||
( # Optional :port
|
||||
:
|
||||
(?P<port>[0-9]{,5}) # Up to 5-digits port
|
||||
)?
|
||||
[/]?
|
||||
$
|
||||
""", url, re.VERBOSE)
|
||||
|
||||
if not match:
|
||||
raise RuntimeError("SMTP URL seems incorrect")
|
||||
|
||||
d = match.groupdict()
|
||||
if d['proto'] is None:
|
||||
d['proto'] = 'smtp'
|
||||
|
||||
if d['port'] is None:
|
||||
d['port'] = 25
|
||||
else:
|
||||
d['port'] = int(d['port'])
|
||||
|
||||
if not 0 < d['port'] < 65536:
|
||||
raise RuntimeError("Incorrect SMTP port")
|
||||
|
||||
return d
|
||||
|
||||
def send_email(self, email_addr, subject, email_text):
|
||||
"""Send an email
|
||||
|
||||
:param email_addr: email address
|
||||
:type email_addr: str.
|
||||
:param subject: subject
|
||||
:type subject: str.
|
||||
:param email_text: email text
|
||||
:type email_text: str.
|
||||
:raises: AAAException if smtp_server and/or sender are not set
|
||||
"""
|
||||
if not (self._conf['fqdn'] and self.sender):
|
||||
raise AAAException("SMTP server or sender not set")
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = self.sender
|
||||
msg['To'] = email_addr
|
||||
if isinstance(email_text, bytes):
|
||||
email_text = email_text.encode('utf-8')
|
||||
|
||||
part = MIMEText(email_text, 'html')
|
||||
msg.attach(part)
|
||||
msg = msg.as_string()
|
||||
|
||||
log.debug("Sending email using %s" % self._conf['fqdn'])
|
||||
|
||||
if self.use_threads:
|
||||
thread = Thread(target=self._send, args=(email_addr, msg))
|
||||
thread.start()
|
||||
self._threads.append(thread)
|
||||
|
||||
else:
|
||||
self._send(email_addr, msg)
|
||||
|
||||
def _send(self, email_addr, msg):
|
||||
"""Deliver an email using SMTP
|
||||
|
||||
:param email_addr: recipient
|
||||
:type email_addr: str.
|
||||
:param msg: email text
|
||||
:type msg: str.
|
||||
"""
|
||||
proto = self._conf['proto']
|
||||
assert proto in ('smtp', 'starttls', 'ssl'), \
|
||||
"Incorrect protocol: %s" % proto
|
||||
|
||||
try:
|
||||
if proto == 'ssl':
|
||||
log.debug("Setting up SSL")
|
||||
session = SMTP_SSL(self._conf['fqdn'], self._conf['port'])
|
||||
else:
|
||||
session = SMTP(self._conf['fqdn'], self._conf['port'])
|
||||
|
||||
if proto == 'starttls':
|
||||
log.debug('Sending EHLO and STARTTLS')
|
||||
session.ehlo()
|
||||
session.starttls()
|
||||
session.ehlo()
|
||||
|
||||
if self._conf['user'] is not None:
|
||||
log.debug('Performing login')
|
||||
session.login(self._conf['user'], self._conf['pass'])
|
||||
|
||||
log.debug('Sending')
|
||||
session.sendmail(self.sender, email_addr, msg)
|
||||
session.quit()
|
||||
log.info('Email sent')
|
||||
|
||||
except Exception as e: # pragma: no cover
|
||||
log.error("Error sending email: %s" % e, exc_info=True)
|
||||
|
||||
def join(self):
|
||||
"""Flush email queue by waiting the completion of the existing threads
|
||||
|
||||
:returns: None
|
||||
"""
|
||||
return [t.join(self.join_timeout) for t in self._threads]
|
||||
|
||||
def __del__(self):
|
||||
"""Class destructor: wait for threads to terminate within a timeout"""
|
||||
try:
|
||||
self.join()
|
||||
except TypeError:
|
||||
pass
|
134
libs/cork/json_backend.py
Normal file
134
libs/cork/json_backend.py
Normal file
|
@ -0,0 +1,134 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
|
||||
"""
|
||||
.. module:: json_backend
|
||||
:synopsis: JSON file-based storage backend.
|
||||
"""
|
||||
|
||||
from logging import getLogger
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError: # pragma: no cover
|
||||
import simplejson as json
|
||||
|
||||
from .base_backend import BackendIOException
|
||||
|
||||
is_py3 = (sys.version_info.major == 3)
|
||||
|
||||
log = getLogger(__name__)
|
||||
|
||||
try:
|
||||
dict.iteritems
|
||||
py23dict = dict
|
||||
except AttributeError:
|
||||
class py23dict(dict):
|
||||
iteritems = dict.items
|
||||
|
||||
class BytesEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if is_py3 and isinstance(obj, bytes):
|
||||
return obj.decode()
|
||||
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class JsonBackend(object):
|
||||
"""JSON file-based storage backend."""
|
||||
|
||||
def __init__(self, directory, users_fname='users',
|
||||
roles_fname='roles', pending_reg_fname='register', initialize=False):
|
||||
"""Data storage class. Handles JSON files
|
||||
|
||||
:param users_fname: users file name (without .json)
|
||||
:type users_fname: str.
|
||||
:param roles_fname: roles file name (without .json)
|
||||
:type roles_fname: str.
|
||||
:param pending_reg_fname: pending registrations file name (without .json)
|
||||
:type pending_reg_fname: str.
|
||||
:param initialize: create empty JSON files (defaults to False)
|
||||
:type initialize: bool.
|
||||
"""
|
||||
assert directory, "Directory name must be valid"
|
||||
self._directory = directory
|
||||
self.users = py23dict()
|
||||
self._users_fname = users_fname
|
||||
self.roles = py23dict()
|
||||
self._roles_fname = roles_fname
|
||||
self._mtimes = py23dict()
|
||||
self._pending_reg_fname = pending_reg_fname
|
||||
self.pending_registrations = py23dict()
|
||||
if initialize:
|
||||
self._initialize_storage()
|
||||
self._refresh() # load users and roles
|
||||
|
||||
def _initialize_storage(self):
|
||||
"""Create empty JSON files"""
|
||||
self._savejson(self._users_fname, {})
|
||||
self._savejson(self._roles_fname, {})
|
||||
self._savejson(self._pending_reg_fname, {})
|
||||
|
||||
def _refresh(self):
|
||||
"""Load users and roles from JSON files, if needed"""
|
||||
self._loadjson(self._users_fname, self.users)
|
||||
self._loadjson(self._roles_fname, self.roles)
|
||||
self._loadjson(self._pending_reg_fname, self.pending_registrations)
|
||||
|
||||
def _loadjson(self, fname, dest):
|
||||
"""Load JSON file located under self._directory, if needed
|
||||
|
||||
:param fname: short file name (without path and .json)
|
||||
:type fname: str.
|
||||
:param dest: destination
|
||||
:type dest: dict
|
||||
"""
|
||||
try:
|
||||
fname = "%s/%s.json" % (self._directory, fname)
|
||||
mtime = os.stat(fname).st_mtime
|
||||
|
||||
if self._mtimes.get(fname, 0) == mtime:
|
||||
# no need to reload the file: the mtime has not been changed
|
||||
return
|
||||
|
||||
with open(fname) as f:
|
||||
json_data = f.read()
|
||||
except Exception as e:
|
||||
raise BackendIOException("Unable to read json file %s: %s" % (fname, e))
|
||||
|
||||
try:
|
||||
json_obj = json.loads(json_data)
|
||||
dest.clear()
|
||||
dest.update(json_obj)
|
||||
self._mtimes[fname] = os.stat(fname).st_mtime
|
||||
except Exception as e:
|
||||
raise BackendIOException("Unable to parse JSON data from %s: %s" \
|
||||
% (fname, e))
|
||||
|
||||
def _savejson(self, fname, obj):
|
||||
"""Save obj in JSON format in a file in self._directory"""
|
||||
fname = "%s/%s.json" % (self._directory, fname)
|
||||
try:
|
||||
with open("%s.tmp" % fname, 'w') as f:
|
||||
json.dump(obj, f, cls=BytesEncoder)
|
||||
f.flush()
|
||||
shutil.move("%s.tmp" % fname, fname)
|
||||
except Exception as e:
|
||||
raise BackendIOException("Unable to save JSON file %s: %s" \
|
||||
% (fname, e))
|
||||
|
||||
def save_users(self):
|
||||
"""Save users in a JSON file"""
|
||||
self._savejson(self._users_fname, self.users)
|
||||
|
||||
def save_roles(self):
|
||||
"""Save roles in a JSON file"""
|
||||
self._savejson(self._roles_fname, self.roles)
|
||||
|
||||
def save_pending_registrations(self):
|
||||
"""Save pending registrations in a JSON file"""
|
||||
self._savejson(self._pending_reg_fname, self.pending_registrations)
|
180
libs/cork/mongodb_backend.py
Normal file
180
libs/cork/mongodb_backend.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
|
||||
"""
|
||||
.. module:: mongodb_backend
|
||||
:synopsis: MongoDB storage backend.
|
||||
"""
|
||||
from logging import getLogger
|
||||
log = getLogger(__name__)
|
||||
|
||||
from .base_backend import Backend, Table
|
||||
|
||||
try:
|
||||
import pymongo
|
||||
is_pymongo_2 = (pymongo.version_tuple[0] == 2)
|
||||
except ImportError: # pragma: no cover
|
||||
pass
|
||||
|
||||
|
||||
class MongoTable(Table):
|
||||
"""Abstract MongoDB Table.
|
||||
Allow dictionary-like access.
|
||||
"""
|
||||
def __init__(self, name, key_name, collection):
|
||||
self._name = name
|
||||
self._key_name = key_name
|
||||
self._coll = collection
|
||||
|
||||
def create_index(self):
|
||||
"""Create collection index."""
|
||||
self._coll.create_index(
|
||||
self._key_name,
|
||||
drop_dups=True,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
return self._coll.count()
|
||||
|
||||
def __contains__(self, value):
|
||||
r = self._coll.find_one({self._key_name: value})
|
||||
return r is not None
|
||||
|
||||
def __iter__(self):
|
||||
"""Iter on dictionary keys"""
|
||||
if is_pymongo_2:
|
||||
r = self._coll.find(fields=[self._key_name,])
|
||||
else:
|
||||
r = self._coll.find(projection=[self._key_name,])
|
||||
|
||||
return (i[self._key_name] for i in r)
|
||||
|
||||
def iteritems(self):
|
||||
"""Iter on dictionary items.
|
||||
|
||||
:returns: generator of (key, value) tuples
|
||||
"""
|
||||
r = self._coll.find()
|
||||
for i in r:
|
||||
d = i.copy()
|
||||
d.pop(self._key_name)
|
||||
d.pop('_id')
|
||||
yield (i[self._key_name], d)
|
||||
|
||||
def pop(self, key_val):
|
||||
"""Remove a dictionary item"""
|
||||
r = self[key_val]
|
||||
self._coll.remove({self._key_name: key_val}, w=1)
|
||||
return r
|
||||
|
||||
|
||||
class MongoSingleValueTable(MongoTable):
|
||||
"""MongoDB table accessible as a simple key -> value dictionary.
|
||||
Used to store roles.
|
||||
"""
|
||||
# Values are stored in a MongoDB "column" named "val"
|
||||
def __init__(self, *args, **kw):
|
||||
super(MongoSingleValueTable, self).__init__(*args, **kw)
|
||||
|
||||
def __setitem__(self, key_val, data):
|
||||
assert not isinstance(data, dict)
|
||||
spec = {self._key_name: key_val}
|
||||
data = {self._key_name: key_val, 'val': data}
|
||||
if is_pymongo_2:
|
||||
self._coll.update(spec, {'$set': data}, upsert=True, w=1)
|
||||
else:
|
||||
self._coll.update_one(spec, {'$set': data}, upsert=True)
|
||||
|
||||
def __getitem__(self, key_val):
|
||||
r = self._coll.find_one({self._key_name: key_val})
|
||||
if r is None:
|
||||
raise KeyError(key_val)
|
||||
|
||||
return r['val']
|
||||
|
||||
class MongoMutableDict(dict):
|
||||
"""Represent an item from a Table. Acts as a dictionary.
|
||||
"""
|
||||
def __init__(self, parent, root_key, d):
|
||||
"""Create a MongoMutableDict instance.
|
||||
:param parent: Table instance
|
||||
:type parent: :class:`MongoTable`
|
||||
"""
|
||||
super(MongoMutableDict, self).__init__(d)
|
||||
self._parent = parent
|
||||
self._root_key = root_key
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
super(MongoMutableDict, self).__setitem__(k, v)
|
||||
spec = {self._parent._key_name: self._root_key}
|
||||
if is_pymongo_2:
|
||||
r = self._parent._coll.update(spec, {'$set': {k: v}}, upsert=True)
|
||||
else:
|
||||
r = self._parent._coll.update_one(spec, {'$set': {k: v}}, upsert=True)
|
||||
|
||||
|
||||
|
||||
class MongoMultiValueTable(MongoTable):
|
||||
"""MongoDB table accessible as a dictionary.
|
||||
"""
|
||||
def __init__(self, *args, **kw):
|
||||
super(MongoMultiValueTable, self).__init__(*args, **kw)
|
||||
|
||||
def __setitem__(self, key_val, data):
|
||||
assert isinstance(data, dict)
|
||||
key_name = self._key_name
|
||||
if key_name in data:
|
||||
assert data[key_name] == key_val
|
||||
else:
|
||||
data[key_name] = key_val
|
||||
|
||||
spec = {key_name: key_val}
|
||||
if u'_id' in data:
|
||||
del(data[u'_id'])
|
||||
|
||||
if is_pymongo_2:
|
||||
self._coll.update(spec, {'$set': data}, upsert=True, w=1)
|
||||
else:
|
||||
self._coll.update_one(spec, {'$set': data}, upsert=True)
|
||||
|
||||
def __getitem__(self, key_val):
|
||||
r = self._coll.find_one({self._key_name: key_val})
|
||||
if r is None:
|
||||
raise KeyError(key_val)
|
||||
|
||||
return MongoMutableDict(self, key_val, r)
|
||||
|
||||
|
||||
class MongoDBBackend(Backend):
|
||||
def __init__(self, db_name='cork', hostname='localhost', port=27017, initialize=False, username=None, password=None):
|
||||
"""Initialize MongoDB Backend"""
|
||||
connection = pymongo.MongoClient(host=hostname, port=port)
|
||||
db = connection[db_name]
|
||||
if username and password:
|
||||
db.authenticate(username, password)
|
||||
self.users = MongoMultiValueTable('users', 'login', db.users)
|
||||
self.pending_registrations = MongoMultiValueTable(
|
||||
'pending_registrations',
|
||||
'pending_registration',
|
||||
db.pending_registrations
|
||||
)
|
||||
self.roles = MongoSingleValueTable('roles', 'role', db.roles)
|
||||
|
||||
if initialize:
|
||||
self._initialize_storage()
|
||||
|
||||
def _initialize_storage(self):
|
||||
"""Create MongoDB indexes."""
|
||||
for c in (self.users, self.roles, self.pending_registrations):
|
||||
c.create_index()
|
||||
|
||||
def save_users(self):
|
||||
pass
|
||||
|
||||
def save_roles(self):
|
||||
pass
|
||||
|
||||
def save_pending_registrations(self):
|
||||
pass
|
23
libs/cork/sessions.py
Normal file
23
libs/cork/sessions.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
import json
|
||||
import base64
|
||||
import hmac
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
def _strcmp(a, b):
|
||||
"""Compares two strings while preventing timing attacks. Execution time
|
||||
is not affected by lenghth of common prefix on strings of the same length"""
|
||||
return not sum(0 if x==y else 1 for x, y in zip(a, b)) and len(a) == len(b)
|
||||
|
||||
class SecureSession(object):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
json()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
base64.b64encode(hmac.new(tob(key), msg).digest())):
|
||||
return pickle.loads(base64.b64decode(msg))
|
204
libs/cork/sqlalchemy_backend.py
Normal file
204
libs/cork/sqlalchemy_backend.py
Normal file
|
@ -0,0 +1,204 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
|
||||
"""
|
||||
.. module:: sqlalchemy_backend
|
||||
:synopsis: SQLAlchemy storage backend.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from logging import getLogger
|
||||
|
||||
from . import base_backend
|
||||
|
||||
log = getLogger(__name__)
|
||||
is_py3 = (sys.version_info.major == 3)
|
||||
|
||||
try:
|
||||
from sqlalchemy import create_engine, delete, select, \
|
||||
Column, ForeignKey, Integer, MetaData, String, Table, Unicode
|
||||
sqlalchemy_available = True
|
||||
except ImportError: # pragma: no cover
|
||||
sqlalchemy_available = False
|
||||
|
||||
|
||||
class SqlRowProxy(dict):
|
||||
def __init__(self, sql_dict, key, *args, **kwargs):
|
||||
dict.__init__(self, *args, **kwargs)
|
||||
self.sql_dict = sql_dict
|
||||
self.key = key
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key, value)
|
||||
if self.sql_dict is not None:
|
||||
self.sql_dict[self.key] = {key: value}
|
||||
|
||||
|
||||
class SqlTable(base_backend.Table):
|
||||
"""Provides dictionary-like access to an SQL table."""
|
||||
|
||||
def __init__(self, engine, table, key_col_name):
|
||||
self._engine = engine
|
||||
self._table = table
|
||||
self._key_col = table.c[key_col_name]
|
||||
|
||||
def _row_to_value(self, row):
|
||||
row_key = row[self._key_col]
|
||||
row_value = SqlRowProxy(self, row_key,
|
||||
((k, row[k]) for k in row.keys() if k != self._key_col.name))
|
||||
return row_key, row_value
|
||||
|
||||
def __len__(self):
|
||||
query = self._table.count()
|
||||
c = self._engine.execute(query).scalar()
|
||||
return int(c)
|
||||
|
||||
def __contains__(self, key):
|
||||
query = select([self._key_col], self._key_col == key)
|
||||
row = self._engine.execute(query).fetchone()
|
||||
return row is not None
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
values = value
|
||||
query = self._table.update().where(self._key_col == key)
|
||||
|
||||
else:
|
||||
values = {self._key_col.name: key}
|
||||
values.update(value)
|
||||
query = self._table.insert()
|
||||
|
||||
self._engine.execute(query.values(**values))
|
||||
|
||||
def __getitem__(self, key):
|
||||
query = select([self._table], self._key_col == key)
|
||||
row = self._engine.execute(query).fetchone()
|
||||
if row is None:
|
||||
raise KeyError(key)
|
||||
return self._row_to_value(row)[1]
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over table index key values"""
|
||||
query = select([self._key_col])
|
||||
result = self._engine.execute(query)
|
||||
for row in result:
|
||||
key = row[0]
|
||||
yield key
|
||||
|
||||
def iteritems(self):
|
||||
"""Iterate over table rows"""
|
||||
query = select([self._table])
|
||||
result = self._engine.execute(query)
|
||||
for row in result:
|
||||
key = row[0]
|
||||
d = self._row_to_value(row)[1]
|
||||
yield (key, d)
|
||||
|
||||
def pop(self, key):
|
||||
query = select([self._table], self._key_col == key)
|
||||
row = self._engine.execute(query).fetchone()
|
||||
if row is None:
|
||||
raise KeyError
|
||||
|
||||
query = delete(self._table, self._key_col == key)
|
||||
self._engine.execute(query)
|
||||
return row
|
||||
|
||||
def insert(self, d):
|
||||
query = self._table.insert(d)
|
||||
self._engine.execute(query)
|
||||
log.debug("%s inserted" % repr(d))
|
||||
|
||||
def empty_table(self):
|
||||
query = self._table.delete()
|
||||
self._engine.execute(query)
|
||||
log.info("Table purged")
|
||||
|
||||
|
||||
class SqlSingleValueTable(SqlTable):
|
||||
def __init__(self, engine, table, key_col_name, col_name):
|
||||
SqlTable.__init__(self, engine, table, key_col_name)
|
||||
self._col_name = col_name
|
||||
|
||||
def _row_to_value(self, row):
|
||||
return row[self._key_col], row[self._col_name]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
SqlTable.__setitem__(self, key, {self._col_name: value})
|
||||
|
||||
|
||||
|
||||
class SqlAlchemyBackend(base_backend.Backend):
|
||||
|
||||
def __init__(self, db_full_url, users_tname='users', roles_tname='roles',
|
||||
pending_reg_tname='register', initialize=False):
|
||||
|
||||
if not sqlalchemy_available:
|
||||
raise RuntimeError("The SQLAlchemy library is not available.")
|
||||
|
||||
self._metadata = MetaData()
|
||||
if initialize:
|
||||
# Create new database if needed.
|
||||
db_url, db_name = db_full_url.rsplit('/', 1)
|
||||
if is_py3 and db_url.startswith('mysql'):
|
||||
print("WARNING: MySQL is not supported under Python3")
|
||||
|
||||
self._engine = create_engine(db_url, encoding='utf-8')
|
||||
try:
|
||||
self._engine.execute("CREATE DATABASE %s" % db_name)
|
||||
except Exception as e:
|
||||
log.info("Failed DB creation: %s" % e)
|
||||
|
||||
# SQLite in-memory database URL: "sqlite://:memory:"
|
||||
if db_name != ':memory:' and not db_url.startswith('postgresql'):
|
||||
self._engine.execute("USE %s" % db_name)
|
||||
|
||||
else:
|
||||
self._engine = create_engine(db_full_url, encoding='utf-8')
|
||||
|
||||
|
||||
self._users = Table(users_tname, self._metadata,
|
||||
Column('username', Unicode(128), primary_key=True),
|
||||
Column('role', ForeignKey(roles_tname + '.role')),
|
||||
Column('hash', String(256), nullable=False),
|
||||
Column('email_addr', String(128)),
|
||||
Column('desc', String(128)),
|
||||
Column('creation_date', String(128), nullable=False),
|
||||
Column('last_login', String(128), nullable=False)
|
||||
|
||||
)
|
||||
self._roles = Table(roles_tname, self._metadata,
|
||||
Column('role', String(128), primary_key=True),
|
||||
Column('level', Integer, nullable=False)
|
||||
)
|
||||
self._pending_reg = Table(pending_reg_tname, self._metadata,
|
||||
Column('code', String(128), primary_key=True),
|
||||
Column('username', Unicode(128), nullable=False),
|
||||
Column('role', ForeignKey(roles_tname + '.role')),
|
||||
Column('hash', String(256), nullable=False),
|
||||
Column('email_addr', String(128)),
|
||||
Column('desc', String(128)),
|
||||
Column('creation_date', String(128), nullable=False)
|
||||
)
|
||||
|
||||
self.users = SqlTable(self._engine, self._users, 'username')
|
||||
self.roles = SqlSingleValueTable(self._engine, self._roles, 'role', 'level')
|
||||
self.pending_registrations = SqlTable(self._engine, self._pending_reg, 'code')
|
||||
|
||||
if initialize:
|
||||
self._initialize_storage(db_name)
|
||||
log.debug("Tables created")
|
||||
|
||||
|
||||
def _initialize_storage(self, db_name):
|
||||
self._metadata.create_all(self._engine)
|
||||
|
||||
def _drop_all_tables(self):
|
||||
for table in reversed(self._metadata.sorted_tables):
|
||||
log.info("Dropping table %s" % repr(table.name))
|
||||
self._engine.execute(table.delete())
|
||||
|
||||
def save_users(self): pass
|
||||
def save_roles(self): pass
|
||||
def save_pending_registrations(self): pass
|
242
libs/cork/sqlite_backend.py
Normal file
242
libs/cork/sqlite_backend.py
Normal file
|
@ -0,0 +1,242 @@
|
|||
# Cork - Authentication module for the Bottle web framework
|
||||
# Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file.
|
||||
# Released under LGPLv3+ license, see LICENSE.txt
|
||||
|
||||
"""
|
||||
.. module:: sqlite_backend
|
||||
:synopsis: SQLite storage backend.
|
||||
"""
|
||||
|
||||
from . import base_backend
|
||||
from logging import getLogger
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
class SqlRowProxy(dict):
|
||||
def __init__(self, table, key, row):
|
||||
li = ((k, v) for (k, ktype), v in zip(table._columns[1:], row[1:]))
|
||||
dict.__init__(self, li)
|
||||
self._table = table
|
||||
self._key = key
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key, value)
|
||||
self._table[self._key] = self
|
||||
|
||||
|
||||
class Table(base_backend.Table):
|
||||
"""Provides dictionary-like access to an SQL table."""
|
||||
|
||||
def __init__(self, backend, table_name):
|
||||
self._backend = backend
|
||||
self._engine = backend.connection
|
||||
self._table_name = table_name
|
||||
self._column_names = [n for n, t in self._columns]
|
||||
self._key_col_num = 0
|
||||
self._key_col_name = self._column_names[self._key_col_num]
|
||||
self._key_col = self._column_names[self._key_col_num]
|
||||
|
||||
def _row_to_value(self, key, row):
|
||||
assert isinstance(row, tuple)
|
||||
row_key = row[self._key_col_num]
|
||||
row_value = SqlRowProxy(self, key, row)
|
||||
return row_key, row_value
|
||||
|
||||
def __len__(self):
|
||||
query = "SELECT count() FROM %s" % self._table_name
|
||||
ret = self._backend.run_query(query)
|
||||
return ret.fetchone()[0]
|
||||
|
||||
def __contains__(self, key):
|
||||
#FIXME: count()
|
||||
query = "SELECT * FROM %s WHERE %s='%s'" % \
|
||||
(self._table_name, self._key_col, key)
|
||||
row = self._backend.fetch_one(query)
|
||||
return row is not None
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Create or update a row"""
|
||||
assert isinstance(value, dict)
|
||||
v, cn = set(value), set(self._column_names[1:])
|
||||
assert not v - cn, repr(v - cn)
|
||||
assert not cn - v, repr(cn - v)
|
||||
|
||||
assert set(value) == set(self._column_names[1:]), "%s %s" % \
|
||||
(repr(set(value)), repr(set(self._column_names[1:])))
|
||||
|
||||
col_values = [key] + [value[k] for k in self._column_names[1:]]
|
||||
|
||||
col_names = ', '.join(self._column_names)
|
||||
question_marks = ', '.join('?' for x in col_values)
|
||||
query = "INSERT OR REPLACE INTO %s (%s) VALUES (%s)" % \
|
||||
(self._table_name, col_names, question_marks)
|
||||
|
||||
ret = self._backend.run_query_using_conversion(query, col_values)
|
||||
|
||||
|
||||
def __getitem__(self, key):
|
||||
query = "SELECT * FROM %s WHERE %s='%s'" % \
|
||||
(self._table_name, self._key_col, key)
|
||||
row = self._backend.fetch_one(query)
|
||||
if row is None:
|
||||
raise KeyError(key)
|
||||
|
||||
return self._row_to_value(key, row)[1]
|
||||
#return dict(zip(self._column_names, row))
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over table index key values"""
|
||||
query = "SELECT %s FROM %s" % (self._key_col, self._table_name)
|
||||
result = self._backend.run_query(query)
|
||||
for row in result:
|
||||
yield row[0]
|
||||
|
||||
def iteritems(self):
|
||||
"""Iterate over table rows"""
|
||||
query = "SELECT * FROM %s" % self._table_name
|
||||
result = self._backend.run_query(query)
|
||||
for row in result:
|
||||
d = dict(zip(self._column_names, row))
|
||||
d.pop(self._key_col)
|
||||
|
||||
yield (self._key_col, d)
|
||||
|
||||
def pop(self, key):
|
||||
d = self.__getitem__(key)
|
||||
query = "DELETE FROM %s WHERE %s='%s'" % \
|
||||
(self._table_name, self._key_col, key)
|
||||
self._backend.fetch_one(query)
|
||||
#FIXME: check deletion
|
||||
return d
|
||||
|
||||
def insert(self, d):
|
||||
raise NotImplementedError
|
||||
|
||||
def empty_table(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def create_table(self):
|
||||
"""Issue table creation"""
|
||||
cc = []
|
||||
for col_name, col_type in self._columns:
|
||||
if col_type == int:
|
||||
col_type = 'INTEGER'
|
||||
elif col_type == str:
|
||||
col_type = 'TEXT'
|
||||
|
||||
if col_name == self._key_col:
|
||||
extras = 'PRIMARY KEY ASC'
|
||||
else:
|
||||
extras = ''
|
||||
|
||||
cc.append("%s %s %s" % (col_name, col_type, extras))
|
||||
|
||||
cc = ','.join(cc)
|
||||
query = "CREATE TABLE %s (%s)" % (self._table_name, cc)
|
||||
self._backend.run_query(query)
|
||||
|
||||
|
||||
class SingleValueTable(Table):
|
||||
def __init__(self, *args):
|
||||
super(SingleValueTable, self).__init__(*args)
|
||||
self._value_col = self._column_names[1]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Create or update a row"""
|
||||
assert not isinstance(value, dict)
|
||||
query = "INSERT OR REPLACE INTO %s (%s, %s) VALUES (?, ?)" % \
|
||||
(self._table_name, self._key_col, self._value_col)
|
||||
|
||||
col_values = (key, value)
|
||||
ret = self._backend.run_query_using_conversion(query, col_values)
|
||||
|
||||
def __getitem__(self, key):
|
||||
query = "SELECT %s FROM %s WHERE %s='%s'" % \
|
||||
(self._value_col, self._table_name, self._key_col, key)
|
||||
row = self._backend.fetch_one(query)
|
||||
if row is None:
|
||||
raise KeyError(key)
|
||||
|
||||
return row[0]
|
||||
|
||||
class UsersTable(Table):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._columns = (
|
||||
('username', str),
|
||||
('role', str),
|
||||
('hash', str),
|
||||
('email_addr', str),
|
||||
('desc', str),
|
||||
('creation_date', str),
|
||||
('last_login', str)
|
||||
)
|
||||
super(UsersTable, self).__init__(*args, **kwargs)
|
||||
|
||||
class RolesTable(SingleValueTable):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._columns = (
|
||||
('role', str),
|
||||
('level', int)
|
||||
)
|
||||
super(RolesTable, self).__init__(*args, **kwargs)
|
||||
|
||||
class PendingRegistrationsTable(Table):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._columns = (
|
||||
('code', str),
|
||||
('username', str),
|
||||
('role', str),
|
||||
('hash', str),
|
||||
('email_addr', str),
|
||||
('desc', str),
|
||||
('creation_date', str)
|
||||
)
|
||||
super(PendingRegistrationsTable, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
class SQLiteBackend(base_backend.Backend):
|
||||
|
||||
def __init__(self, filename, users_tname='users', roles_tname='roles',
|
||||
pending_reg_tname='register', initialize=False):
|
||||
|
||||
self._filename = filename
|
||||
|
||||
self.users = UsersTable(self, users_tname)
|
||||
self.roles = RolesTable(self, roles_tname)
|
||||
self.pending_registrations = PendingRegistrationsTable(self, pending_reg_tname)
|
||||
|
||||
if initialize:
|
||||
self.users.create_table()
|
||||
self.roles.create_table()
|
||||
self.pending_registrations.create_table()
|
||||
log.debug("Tables created")
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
try:
|
||||
return self._connection
|
||||
except AttributeError:
|
||||
import sqlite3
|
||||
self._connection = sqlite3.connect(self._filename)
|
||||
return self._connection
|
||||
|
||||
def run_query(self, query):
|
||||
return self._connection.execute(query)
|
||||
|
||||
def run_query_using_conversion(self, query, args):
|
||||
return self._connection.execute(query, args)
|
||||
|
||||
def fetch_one(self, query):
|
||||
return self._connection.execute(query).fetchone()
|
||||
|
||||
def _initialize_storage(self, db_name):
|
||||
raise NotImplementedError
|
||||
|
||||
def _drop_all_tables(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def save_users(self): pass
|
||||
def save_roles(self): pass
|
||||
def save_pending_registrations(self): pass
|
|
@ -19,6 +19,7 @@ urllib3<1.23,>=1.21.1
|
|||
waitress>=1.1.0
|
||||
configparser>=3.5.0
|
||||
backports.functools-lru-cache>=1.5
|
||||
beaker>=1.10.0
|
||||
|
||||
#Subliminal requirements
|
||||
click>=6.7
|
||||
|
|
71
views/login.tpl
Normal file
71
views/login.tpl
Normal file
|
@ -0,0 +1,71 @@
|
|||
<html>
|
||||
<head>
|
||||
<!DOCTYPE html>
|
||||
<script src="{{base_url}}static/jquery/jquery-latest.min.js"></script>
|
||||
<script src="{{base_url}}static/semantic/semantic.min.js"></script>
|
||||
<script src="{{base_url}}static/jquery/tablesort.js"></script>
|
||||
<link rel="stylesheet" href="{{base_url}}static/semantic/semantic.min.css">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{base_url}}static/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{base_url}}static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{base_url}}static/favicon-16x16.png">
|
||||
<link rel="mask-icon" href="{{base_url}}static/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<link rel="shortcut icon" href="{{base_url}}static/favicon.ico">
|
||||
<meta name="msapplication-config" content="{{base_url}}static/browserconfig.xml">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<title>Login - Bazarr</title>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #272727;
|
||||
}
|
||||
body > .grid {
|
||||
height: 100%;
|
||||
}
|
||||
.image {
|
||||
margin-top: 0px;
|
||||
}
|
||||
.column {
|
||||
max-width: 450px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="ui middle aligned center aligned grid">
|
||||
<div class="column">
|
||||
<h2 class="ui small image">
|
||||
<img src="{{base_url}}static/logo128.png" class="image">
|
||||
</h2>
|
||||
<form class="ui large form" method="post" action="{{base_url}}login" name="login">
|
||||
<div class="ui segment">
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
<i class="user icon"></i>
|
||||
<input type="text" name="username" placeholder="Username" autofocus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui left icon input">
|
||||
<i class="lock icon"></i>
|
||||
<input type="password" name="password" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="ui fluid large blue submit button"> Login </button>
|
||||
</div>
|
||||
|
||||
% if msg == 'fail':
|
||||
<div class="ui red message" role="alert">
|
||||
Incorrect Username or Password.
|
||||
</div>
|
||||
% end
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -42,7 +42,7 @@
|
|||
% include('menu.tpl')
|
||||
|
||||
<div id="fondblanc" class="ui container">
|
||||
<form name="settings_form" id="settings_form" action="{{base_url}}save_settings" method="post" class="ui form">
|
||||
<form name="settings_form" id="settings_form" action="{{base_url}}save_settings" method="post" class="ui form" autocomplete="off">
|
||||
<div id="form_validation_error" class="ui error message">
|
||||
<p>Some fields are in error and you can't save settings until you have corrected them. Be sure to check in every tabs.</p>
|
||||
</div>
|
||||
|
@ -191,14 +191,17 @@
|
|||
<div class="ui grid">
|
||||
<div class="middle aligned row">
|
||||
<div class="right aligned four wide column">
|
||||
<label>Use basic authentication</label>
|
||||
<label>Authentication</label>
|
||||
</div>
|
||||
<div class="one wide column">
|
||||
<div id="settings_use_auth" class="ui toggle checkbox" data-enabled={{settings_auth[0]}}>
|
||||
<input name="settings_general_auth_enabled" type="checkbox">
|
||||
<div class="five wide column">
|
||||
<select name="settings_auth_type" id="settings_auth_type" class="ui fluid selection dropdown">
|
||||
<option value="None">None</option>
|
||||
<option value="basic">Basic (Browser Popup)</option>
|
||||
<option value="form">Forms (Login Page)</option>
|
||||
</select>
|
||||
<label></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="collapsed center aligned column">
|
||||
<div class="ui basic icon" data-tooltip="Requires restart to take effect" data-inverted="">
|
||||
|
@ -208,41 +211,41 @@
|
|||
|
||||
<div class="collapsed column">
|
||||
<div class="collapsed center aligned column">
|
||||
<div class="ui basic icon" data-tooltip="Enable basic authentication to access Bazarr." data-inverted="">
|
||||
<div class="ui basic icon" data-tooltip="Require Username and Password to access Bazarr." data-inverted="">
|
||||
<i class="help circle large icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned row">
|
||||
<div class="auth_option middle aligned row">
|
||||
<div class="right aligned four wide column">
|
||||
<label>Username</label>
|
||||
</div>
|
||||
<div class="five wide column">
|
||||
<div class='field'>
|
||||
<div class="ui fluid input">
|
||||
<input id="settings_general_auth_username" name="settings_general_auth_username" type="text" value="{{settings_auth[1]}}">
|
||||
<input id="settings_auth_username" name="settings_auth_username" type="text" autocomplete="nope" value="{{settings_auth[1]}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="middle aligned row">
|
||||
<div class="auth_option middle aligned row">
|
||||
<div class="right aligned four wide column">
|
||||
<label>Password</label>
|
||||
</div>
|
||||
<div class="five wide column">
|
||||
<div class='field'>
|
||||
<div class="ui fluid input">
|
||||
<input id="settings_general_auth_password" name="settings_general_auth_password" type="password" value="{{settings_auth[2]}}">
|
||||
<input id="settings_auth_password" name="settings_auth_password" type="password" autocomplete="new-password" value="{{settings_auth[2]}}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapsed column">
|
||||
<div class="collapsed center aligned column">
|
||||
<div class="ui basic icon" data-tooltip="Basic auth transmit username and password in clear over the network. You should add SSL encryption trough a reverse proxy." data-inverted="">
|
||||
<div class="ui basic icon" data-tooltip="Authentication send username and password in clear over the network. You should add SSL encryption trough a reverse proxy." data-inverted="">
|
||||
<i class="help circle large icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1320,26 +1323,6 @@
|
|||
$("#settings_adaptive_searching").checkbox('uncheck');
|
||||
}
|
||||
|
||||
if ($('#settings_use_auth').data("enabled") == "True") {
|
||||
$("#settings_use_auth").checkbox('check');
|
||||
$("#settings_general_auth_username").parent().removeClass('disabled');
|
||||
$("#settings_general_auth_password").parent().removeClass('disabled');
|
||||
} else {
|
||||
$("#settings_use_auth").checkbox('uncheck');
|
||||
$("#settings_general_auth_username").parent().addClass('disabled');
|
||||
$("#settings_general_auth_password").parent().addClass('disabled');
|
||||
}
|
||||
|
||||
$("#settings_use_auth").change(function(i, obj) {
|
||||
if ($("#settings_use_auth").checkbox('is checked')) {
|
||||
$("#settings_general_auth_username").parent().removeClass('disabled');
|
||||
$("#settings_general_auth_password").parent().removeClass('disabled');
|
||||
} else {
|
||||
$("#settings_general_auth_username").parent().addClass('disabled');
|
||||
$("#settings_general_auth_password").parent().addClass('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
if ($('#settings_use_postprocessing').data("postprocessing") == "True") {
|
||||
$("#settings_use_postprocessing").checkbox('check');
|
||||
$("#settings_general_postprocessing_cmd_div").removeClass('disabled');
|
||||
|
@ -1396,6 +1379,19 @@
|
|||
}
|
||||
});
|
||||
|
||||
if ($('#settings_auth_type').val() == "None") {
|
||||
$('.auth_option').hide();
|
||||
};
|
||||
|
||||
$('#settings_auth_type').dropdown('setting', 'onChange', function(){
|
||||
if ($('#settings_auth_type').val() == "None") {
|
||||
$('.auth_option').hide();
|
||||
}
|
||||
else {
|
||||
$('.auth_option').show();
|
||||
};
|
||||
});
|
||||
|
||||
$('#settings_languages').dropdown('setting', 'onAdd', function(val, txt){
|
||||
$("#settings_serie_default_languages").append(
|
||||
$("<option></option>").attr("value", val).text(txt)
|
||||
|
@ -1554,6 +1550,8 @@
|
|||
%if settings_general[19] is not None:
|
||||
$('#settings_movie_default_languages').dropdown('set selected',{{!settings_general[19]}});
|
||||
%end
|
||||
$('#settings_auth_type').dropdown('clear');
|
||||
$('#settings_auth_type').dropdown('set selected','{{!settings_auth[0]}}');
|
||||
$('#settings_branch').dropdown();
|
||||
$('#settings_sonarr_sync').dropdown();
|
||||
$('#settings_radarr_sync').dropdown();
|
||||
|
@ -1584,19 +1582,12 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
settings_general_auth_username : {
|
||||
depends: 'settings_general_auth_enabled',
|
||||
settings_auth_password : {
|
||||
depends: 'settings_auth_username',
|
||||
rules : [
|
||||
{
|
||||
type : 'empty'
|
||||
}
|
||||
]
|
||||
},
|
||||
settings_general_auth_password : {
|
||||
depends: 'settings_general_auth_enabled',
|
||||
rules : [
|
||||
{
|
||||
type : 'empty'
|
||||
type : 'empty',
|
||||
prompt : 'This field must have a value and you must type it again if you change your username.'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -1733,6 +1724,12 @@
|
|||
$('#loader').removeClass('active');
|
||||
})
|
||||
|
||||
$('#settings_auth_username').keyup(function() {
|
||||
$('#settings_auth_password').val('');
|
||||
$('.form').form('validate form');
|
||||
$('#loader').removeClass('active');
|
||||
})
|
||||
|
||||
$('#sonarr_validate').click(function() {
|
||||
if ($('#sonarr_ssl_div').checkbox('is checked')) {
|
||||
sonarr_url = 'https://';
|
||||
|
|
Loading…
Reference in a new issue