mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-10 15:32:38 -07:00
Initial implementation of login control
This commit is contained in:
parent
0aa2537d1e
commit
51a12099e4
24 changed files with 541 additions and 224 deletions
|
@ -74,6 +74,8 @@ UMASK = None
|
|||
|
||||
POLLING_FAILOVER = False
|
||||
|
||||
HTTP_ROOT = None
|
||||
|
||||
DEV = False
|
||||
|
||||
|
||||
|
|
158
plexpy/webauth.py
Normal file
158
plexpy/webauth.py
Normal file
|
@ -0,0 +1,158 @@
|
|||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# PlexPy 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
# http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
|
||||
# Form based authentication for CherryPy. Requires the
|
||||
# Session tool to be loaded.
|
||||
|
||||
import cherrypy
|
||||
from cgi import escape
|
||||
|
||||
import plexpy
|
||||
from plexpy import logger
|
||||
|
||||
|
||||
SESSION_KEY = '_cp_username'
|
||||
|
||||
def check_credentials(username, password):
|
||||
"""Verifies credentials for username and password.
|
||||
Returns None on success or a string describing the error on failure"""
|
||||
# Adapt to your needs
|
||||
if username == plexpy.CONFIG.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||
return None
|
||||
else:
|
||||
return u"Incorrect username or password."
|
||||
|
||||
# An example implementation which uses an ORM could be:
|
||||
# u = User.get(username)
|
||||
# if u is None:
|
||||
# return u"Username %s is unknown to me." % username
|
||||
# if u.password != md5.new(password).hexdigest():
|
||||
# return u"Incorrect password"
|
||||
|
||||
def check_auth(*args, **kwargs):
|
||||
"""A tool that looks in config for 'auth.require'. If found and it
|
||||
is not None, a login is required and the entry is evaluated as a list of
|
||||
conditions that the user must fulfill"""
|
||||
conditions = cherrypy.request.config.get('auth.require', None)
|
||||
if conditions is not None:
|
||||
username = cherrypy.session.get(SESSION_KEY)
|
||||
if username:
|
||||
cherrypy.request.login = username
|
||||
for condition in conditions:
|
||||
# A condition is just a callable that returns true or false
|
||||
if not condition():
|
||||
raise cherrypy.HTTPRedirect("auth/login")
|
||||
else:
|
||||
raise cherrypy.HTTPRedirect("auth/login")
|
||||
|
||||
def require(*conditions):
|
||||
"""A decorator that appends conditions to the auth.require config
|
||||
variable."""
|
||||
def decorate(f):
|
||||
if not hasattr(f, '_cp_config'):
|
||||
f._cp_config = dict()
|
||||
if 'auth.require' not in f._cp_config:
|
||||
f._cp_config['auth.require'] = []
|
||||
f._cp_config['auth.require'].extend(conditions)
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
# Conditions are callables that return True
|
||||
# if the user fulfills the conditions they define, False otherwise
|
||||
#
|
||||
# They can access the current username as cherrypy.request.login
|
||||
#
|
||||
# Define those at will however suits the application.
|
||||
|
||||
def member_of(groupname):
|
||||
def check():
|
||||
# replace with actual check if <username> is in <groupname>
|
||||
return cherrypy.request.login == plexpy.CONFIG.HTTP_USERNAME and groupname == 'admin'
|
||||
return check
|
||||
|
||||
def name_is(reqd_username):
|
||||
return lambda: reqd_username == cherrypy.request.login
|
||||
|
||||
# These might be handy
|
||||
|
||||
def any_of(*conditions):
|
||||
"""Returns True if any of the conditions match"""
|
||||
def check():
|
||||
for c in conditions:
|
||||
if c():
|
||||
return True
|
||||
return False
|
||||
return check
|
||||
|
||||
# By default all conditions are required, but this might still be
|
||||
# needed if you want to use it inside of an any_of(...) condition
|
||||
def all_of(*conditions):
|
||||
"""Returns True if all of the conditions match"""
|
||||
def check():
|
||||
for c in conditions:
|
||||
if not c():
|
||||
return False
|
||||
return True
|
||||
return check
|
||||
|
||||
|
||||
# Controller to provide login and logout actions
|
||||
|
||||
class AuthController(object):
|
||||
|
||||
def on_login(self, username):
|
||||
"""Called on successful login"""
|
||||
logger.debug(u"User '%s' logged into PlexPy." % username)
|
||||
|
||||
def on_logout(self, username):
|
||||
"""Called on logout"""
|
||||
logger.debug(u"User '%s' logged out of PlexPy." % username)
|
||||
|
||||
def get_loginform(self, username="", msg=""):
|
||||
from plexpy.webserve import serve_template
|
||||
|
||||
username = escape(username, True)
|
||||
|
||||
return serve_template(templatename="login.html", title="Welcome", username=username, msg=msg)
|
||||
|
||||
@cherrypy.expose
|
||||
def login(self, username=None, password=None, remember_me=0):
|
||||
if username is None or password is None:
|
||||
return self.get_loginform()
|
||||
|
||||
error_msg = check_credentials(username, password)
|
||||
|
||||
if error_msg:
|
||||
logger.debug(u"Invalid login attempt from '%s'." % username)
|
||||
return self.get_loginform(username, error_msg)
|
||||
else:
|
||||
cherrypy.session.regenerate()
|
||||
cherrypy.session[SESSION_KEY] = cherrypy.request.login = username
|
||||
self.on_login(username)
|
||||
raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT or "/")
|
||||
|
||||
@cherrypy.expose
|
||||
def logout(self):
|
||||
sess = cherrypy.session
|
||||
username = sess.get(SESSION_KEY, None)
|
||||
sess[SESSION_KEY] = None
|
||||
|
||||
if username:
|
||||
cherrypy.request.login = None
|
||||
self.on_logout(username)
|
||||
raise cherrypy.HTTPRedirect("login")
|
|
@ -16,6 +16,7 @@
|
|||
from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, \
|
||||
datafactory, graphs, users, libraries, database, web_socket
|
||||
from plexpy.helpers import checked, addtoapi, get_ip, create_https_certificates
|
||||
from plexpy.webauth import AuthController, require, member_of, name_is
|
||||
|
||||
from mako.lookup import TemplateLookup
|
||||
from mako import exceptions
|
||||
|
@ -49,17 +50,20 @@ def serve_template(templatename, **kwargs):
|
|||
|
||||
try:
|
||||
template = _hplookup.get_template(templatename)
|
||||
return template.render(server_name=server_name, **kwargs)
|
||||
return template.render(server_name=server_name, http_root=plexpy.HTTP_ROOT, **kwargs)
|
||||
except:
|
||||
return exceptions.html_error_template().render()
|
||||
|
||||
|
||||
class WebInterface(object):
|
||||
|
||||
auth = AuthController()
|
||||
|
||||
def __init__(self):
|
||||
self.interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/')
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def index(self):
|
||||
if plexpy.CONFIG.FIRST_RUN_COMPLETE:
|
||||
raise cherrypy.HTTPRedirect("home")
|
||||
|
@ -142,6 +146,7 @@ class WebInterface(object):
|
|||
##### Home #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def home(self):
|
||||
config = {
|
||||
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
|
||||
|
@ -270,6 +275,7 @@ class WebInterface(object):
|
|||
##### Libraries #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def libraries(self):
|
||||
config = {
|
||||
"update_section_ids": plexpy.CONFIG.UPDATE_SECTION_IDS
|
||||
|
@ -578,6 +584,7 @@ class WebInterface(object):
|
|||
##### Users #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def users(self):
|
||||
return serve_template(templatename="users.html", title="Users")
|
||||
|
||||
|
@ -749,6 +756,7 @@ class WebInterface(object):
|
|||
##### History #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def history(self):
|
||||
return serve_template(templatename="history.html", title="History")
|
||||
|
||||
|
@ -837,6 +845,7 @@ class WebInterface(object):
|
|||
##### Graphs #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def graphs(self):
|
||||
|
||||
config = {
|
||||
|
@ -1024,6 +1033,7 @@ class WebInterface(object):
|
|||
##### Sync #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def sync(self):
|
||||
return serve_template(templatename="sync.html", title="Synced Items")
|
||||
|
||||
|
@ -1049,6 +1059,7 @@ class WebInterface(object):
|
|||
##### Logs #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def logs(self):
|
||||
return serve_template(templatename="logs.html", title="Log", lineList=plexpy.LOG_LIST)
|
||||
|
||||
|
@ -1167,6 +1178,7 @@ class WebInterface(object):
|
|||
##### Settings #####
|
||||
|
||||
@cherrypy.expose
|
||||
@require()
|
||||
def settings(self):
|
||||
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
||||
interface_list = [name for name in os.listdir(interface_dir) if
|
||||
|
|
|
@ -17,7 +17,7 @@ import os
|
|||
import sys
|
||||
|
||||
import cherrypy
|
||||
from plexpy import logger
|
||||
from plexpy import logger, webauth
|
||||
import plexpy
|
||||
from plexpy.helpers import create_https_certificates
|
||||
from plexpy.webserve import WebInterface
|
||||
|
@ -50,7 +50,7 @@ def initialize(options):
|
|||
'server.thread_pool': 10,
|
||||
'tools.encode.on': True,
|
||||
'tools.encode.encoding': 'utf-8',
|
||||
'tools.decode.on': True,
|
||||
'tools.decode.on': True
|
||||
}
|
||||
|
||||
if enable_https:
|
||||
|
@ -64,8 +64,17 @@ def initialize(options):
|
|||
options_dict['environment'] = "test_suite"
|
||||
options_dict['engine.autoreload.on'] = True
|
||||
|
||||
logger.info("Starting PlexPy web server on %s://%s:%d/", protocol,
|
||||
options['http_host'], options['http_port'])
|
||||
if options['http_password']:
|
||||
logger.info("Web server authentication is enabled, username is '%s'", options['http_username'])
|
||||
options_dict['tools.sessions.on'] = True
|
||||
options_dict['tools.auth.on'] = True
|
||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
|
||||
|
||||
if not options['http_root'] or options['http_root'] == '/':
|
||||
plexpy.HTTP_ROOT = options['http_root'] = '/'
|
||||
else:
|
||||
plexpy.HTTP_ROOT = options['http_root'] = '/' + options['http_root'].strip('/') + '/'
|
||||
|
||||
cherrypy.config.update(options_dict)
|
||||
|
||||
conf = {
|
||||
|
@ -83,15 +92,27 @@ def initialize(options):
|
|||
},
|
||||
'/images': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "images"
|
||||
'tools.staticdir.dir': "interfaces/default/images"
|
||||
},
|
||||
'/css': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "css"
|
||||
'tools.staticdir.dir': "interfaces/default/css"
|
||||
},
|
||||
'/fonts': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "interfaces/default/fonts"
|
||||
},
|
||||
'/js': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "js"
|
||||
'tools.staticdir.dir': "interfaces/default/js"
|
||||
},
|
||||
'/json': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "interfaces/default/json"
|
||||
},
|
||||
'/xml': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "interfaces/default/xml"
|
||||
},
|
||||
'/cache': {
|
||||
'tools.staticdir.on': True,
|
||||
|
@ -100,27 +121,19 @@ def initialize(options):
|
|||
'/favicon.ico': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': os.path.abspath(os.path.join(plexpy.PROG_DIR, 'data/interfaces/default/images/favicon.ico'))
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
if options['http_password']:
|
||||
logger.info("Web server authentication is enabled, username is '%s'", options['http_username'])
|
||||
|
||||
conf['/'].update({
|
||||
'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'PlexPy web server',
|
||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
|
||||
options['http_username']: options['http_password']
|
||||
})
|
||||
})
|
||||
conf['/api'] = {'tools.auth_basic.on': False}
|
||||
conf['/api'] = {'tools.auth.on': False}
|
||||
|
||||
# Prevent time-outs
|
||||
cherrypy.engine.timeout_monitor.unsubscribe()
|
||||
cherrypy.tree.mount(WebInterface(), str(options['http_root']), config=conf)
|
||||
cherrypy.tree.mount(WebInterface(), options['http_root'], config=conf)
|
||||
|
||||
try:
|
||||
logger.info("Starting PlexPy web server on %s://%s:%d%s", protocol,
|
||||
options['http_host'], options['http_port'], options['http_root'])
|
||||
cherrypy.process.servers.check_port(str(options['http_host']), options['http_port'])
|
||||
if not plexpy.DEV:
|
||||
cherrypy.server.start()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue