Skip to content

Commit

Permalink
Merge pull request ceph#22 from rjfd/wip-oa-mgr-controllers
Browse files Browse the repository at this point in the history
mgr/dashboard_v2: Refactoring and support for controller decorator to make development faster
  • Loading branch information
sebastian-philipp committed Jan 25, 2018
2 parents 04de96f + e53f90c commit 5405126
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 216 deletions.
62 changes: 62 additions & 0 deletions src/pybind/mgr/dashboard_v2/README.rst
Expand Up @@ -83,7 +83,69 @@ is located)::

$ tox


If you just want to run a single tox environment, for instance only run the
linting tools::

$ tox -e lint

Developer Notes
---------------

How to add a new controller?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you want to add a new endpoint to the backend, you just need to add
a class decorated with ``ApiController`` in a python file located under the
``controllers`` directory. The dashboard plugin will automatically load your
new controller upon start.

For example create a file ``ping2.py`` under ``controllers`` directory with the
following code::

import cherrypy
from ..tools import ApiController

@ApiController('ping2')
class Ping2(object):
@cherrypy.expose
def default(self, *args):
return "Hello"

Reload the dashboard plugin, and then you can access the above controller
from the web browser using the URL http://mgr_hostname:8080/api/ping2

We also provide a simple mechanism to create REST based controllers using the
``RESTController`` class.

For example, we can adapt the above controller to return JSON when accessing
the endpoint with a GET request::

import cherrypy
from ..tools import ApiController, RESTController

@ApiController('ping2')
class Ping2(RESTController):
def list(self):
return {"msg": "Hello"}


How to restrict access to a controller?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you require that only authenticated users can access you controller, just
add the ``AuthRequired`` decorator to your controller class.

Example::

import cherrypy
from ..tools import ApiController, AuthRequired, RESTController

@ApiController('ping2')
@AuthRequired
class Ping2(RESTController):
def list(self):
return {"msg": "Hello"}

Now only authenticated users will be able to "ping" your controller.

Empty file.
@@ -1,14 +1,16 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

import bcrypt
import cherrypy
import time
import sys

from cherrypy import tools
from ..tools import ApiController, AuthRequired, RESTController


class Auth(object):
@ApiController('auth')
class Auth(RESTController):
"""
Provide login and logout actions.
Expand All @@ -27,63 +29,61 @@ class Auth(object):

DEFAULT_SESSION_EXPIRE = 1200.0

@staticmethod
def password_hash(password, salt_password=None):
if not salt_password:
salt_password = bcrypt.gensalt()
if sys.version_info > (3, 0):
return bcrypt.hashpw(password, salt_password)
else:
return bcrypt.hashpw(password.encode('utf8'), salt_password)
def __init__(self):
self._mod = Auth._mgr_module_
self._log = self._mod.log

def __init__(self, module):
self.module = module
self.log = self.module.log

@cherrypy.expose
@cherrypy.tools.allow(methods=['POST'])
@tools.json_out()
def login(self, username=None, password=None):
@RESTController.args_from_json
def create(self, username, password):
now = time.time()
config_username = self.module.get_localized_config('username', None)
config_password = self.module.get_localized_config('password', None)
config_username = self._mod.get_localized_config('username', None)
config_password = self._mod.get_localized_config('password', None)
hash_password = Auth.password_hash(password,
config_password)
if username == config_username and hash_password == config_password:
cherrypy.session.regenerate()
cherrypy.session[Auth.SESSION_KEY] = username
cherrypy.session[Auth.SESSION_KEY_TS] = now
self.log.debug('Login successful')
self._log.debug("Login successful")
return {'username': username}
else:
cherrypy.response.status = 403
self.log.debug('Login fail')
self._log.debug("Login fail")
return {'detail': 'Invalid credentials'}

@cherrypy.expose
@cherrypy.tools.allow(methods=['POST'])
def logout(self):
self.log.debug('Logout successful')
def bulk_delete(self):
self._log.debug("Logout successful")
cherrypy.session[Auth.SESSION_KEY] = None
cherrypy.session[Auth.SESSION_KEY_TS] = None

def check_auth(self):
@staticmethod
def password_hash(password, salt_password=None):
if not salt_password:
salt_password = bcrypt.gensalt()
if sys.version_info > (3, 0):
return bcrypt.hashpw(password, salt_password)
else:
return bcrypt.hashpw(password.encode('utf8'), salt_password)

@staticmethod
def check_auth():
module = Auth._mgr_module_
username = cherrypy.session.get(Auth.SESSION_KEY)
if not username:
self.log.debug('Unauthorized access to {}'.format(cherrypy.url(
module.log.debug('Unauthorized access to {}'.format(cherrypy.url(
relative='server')))
raise cherrypy.HTTPError(401, 'You are not authorized to access '
'that resource')
now = time.time()
expires = float(self.module.get_localized_config(
expires = float(module.get_localized_config(
'session-expire',
Auth.DEFAULT_SESSION_EXPIRE))
if expires > 0:
username_ts = cherrypy.session.get(Auth.SESSION_KEY_TS, None)
if username_ts and float(username_ts) < (now - expires):
cherrypy.session[Auth.SESSION_KEY] = None
cherrypy.session[Auth.SESSION_KEY_TS] = None
self.log.debug('Session expired.')
module.log.debug("Session expired.")
raise cherrypy.HTTPError(401,
'Session expired. You are not '
'authorized to access that resource')
Expand Down
27 changes: 27 additions & 0 deletions src/pybind/mgr/dashboard_v2/controllers/ping.py
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import

import cherrypy

from ..tools import ApiController, AuthRequired, RESTController


@ApiController('ping')
@AuthRequired()
class Ping(object):
@cherrypy.expose
def default(self, *args):
return "pong"


@ApiController('echo1')
class EchoArgs(RESTController):
@RESTController.args_from_json
def create(self, msg):
return {'echo': msg}


@ApiController('echo2')
class Echo(RESTController):
def create(self, data):
return {'echo': data['msg']}
63 changes: 15 additions & 48 deletions src/pybind/mgr/dashboard_v2/module.py
Expand Up @@ -8,10 +8,11 @@
import os
import cherrypy
from cherrypy import tools

from .auth import Auth
from mgr_module import MgrModule

from .controllers.auth import Auth
from .tools import load_controllers


# cherrypy likes to sys.exit on error. don't let it take us down too!
def os_exit_noop(*args):
Expand Down Expand Up @@ -54,24 +55,10 @@ def serve(self):
'server.socket_host': server_addr,
'server.socket_port': int(server_port),
})
auth = Auth(self)
cherrypy.tools.autenticate = cherrypy.Tool('before_handler',
auth.check_auth)
noauth_required_config = {
'/': {
'tools.autenticate.on': False,
'tools.sessions.on': True
}
}
auth_required_config = {
'/': {
'tools.autenticate.on': True,
'tools.sessions.on': True
}
}
cherrypy.tree.mount(auth, "/api/auth", config=noauth_required_config)
cherrypy.tree.mount(Module.HelloWorld(self), "/api/hello",
config=auth_required_config)
Auth.check_auth)

cherrypy.tree.mount(Module.ApiRoot(self), "/api")
cherrypy.engine.start()
self.log.info("Waiting for engine...")
cherrypy.engine.block()
Expand All @@ -92,32 +79,12 @@ def handle_command(self, cmd):
return (-errno.EINVAL, '', 'Command not found \'{0}\''.format(
cmd['prefix']))

class HelloWorld(object):

"""
Hello World.
"""

def __init__(self, module):
self.module = module
self.log = module.log
self.log.warn("Initiating WebServer CherryPy")

@cherrypy.expose
def index(self):
"""
WS entrypoint
"""

return "Hello World!"

@cherrypy.expose
@tools.json_out()
def ping(self):
"""
Ping endpoint
"""

return "pong"
class ApiRoot(object):
def __init__(self, mgrmod):
ctrls = load_controllers(mgrmod)
mgrmod.log.debug("loaded controllers: {}".format(ctrls))
for ctrl in ctrls:
mgrmod.log.warn("adding controller: {} -> {}"
.format(ctrl.__name__, ctrl._cp_path_))
ins = ctrl()
setattr(Module.ApiRoot, ctrl._cp_path_, ins)
100 changes: 0 additions & 100 deletions src/pybind/mgr/dashboard_v2/restresource.py

This file was deleted.

0 comments on commit 5405126

Please sign in to comment.