Initial implementation of login control

This commit is contained in:
JonnyWong16 2016-04-20 21:25:19 -07:00
parent 0aa2537d1e
commit 51a12099e4
24 changed files with 541 additions and 224 deletions

View file

@ -74,6 +74,8 @@ UMASK = None
POLLING_FAILOVER = False
HTTP_ROOT = None
DEV = False

158
plexpy/webauth.py Normal file
View 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")

View file

@ -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

View file

@ -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()