273 lines
9.0 KiB
ReStructuredText
273 lines
9.0 KiB
ReStructuredText
=================
|
|
Advanced Features
|
|
=================
|
|
|
|
*Milla* contains several powerful tools that allow web developers complete
|
|
control over how their applications behave.
|
|
|
|
.. contents:: Contents
|
|
:local:
|
|
|
|
Propagating Configuration
|
|
=========================
|
|
|
|
While one possible way for :term:`controller` callables to obtain configuration
|
|
information would be for them to read it each time a request is made, it would
|
|
be extremely inefficient. To help with this, *Milla* provides a simple
|
|
configuration dictionary that can be populated when the
|
|
:py:class:`~milla.app.Application` is created and will be available to
|
|
controllers as the :py:attr:`~milla.Request.config` attribute of the request.
|
|
|
|
.. code-block:: python
|
|
|
|
def controller(request):
|
|
if request.config['t_virus'] == 'escaped':
|
|
return 'Zombies!'
|
|
else:
|
|
return 'Raccoon City is safe, for now'
|
|
|
|
router = milla.dispatch.routing.Router()
|
|
router.add_route('/', controller)
|
|
application = milla.Application(router)
|
|
application.config['t_virus'] = 'contained'
|
|
|
|
*Milla* provides a simple utility called :py:func:`~milla.util.read_config`
|
|
that can produce a flat dictionary from a standard configuration file:
|
|
|
|
.. code-block:: ini
|
|
|
|
; umbrella.ini
|
|
[t_virus]
|
|
status = escaped
|
|
|
|
.. code-block:: python
|
|
|
|
# app.py
|
|
class Root(object):
|
|
|
|
def __call__(self, request):
|
|
if request.config['t_virus.status'] == 'escaped':
|
|
return 'Zombies!'
|
|
else:
|
|
return 'Raccoon City is safe, for now'
|
|
|
|
application = milla.Application(Root())
|
|
application.config.update(read_config('umbrella.ini'))
|
|
|
|
Notice that the section name appears in the dictionary key as well as the
|
|
option name, separated by a dot (``.``). This allows you to specify have
|
|
multiple options with the same name, as long as they are in different sections.
|
|
|
|
Allowing Various HTTP Methods
|
|
=============================
|
|
|
|
By default, *Milla* will reject HTTP requests using methods other than ``GET``,
|
|
``HEAD``, or ``OPTIONS`` by returning an `HTTP 405`_ response. If you need a
|
|
controller callable to accept these requests, you need to explicitly specify
|
|
which methods are allowed.
|
|
|
|
To change the request methods that a controller callable accepts, use the
|
|
:py:meth:`~milla.allow` decorator.
|
|
|
|
.. code-block:: python
|
|
|
|
@milla.allow('GET', 'HEAD', 'POST')
|
|
def controller(request):
|
|
response = request.ResponseClass()
|
|
if request.method == 'POST':
|
|
release_t_virus()
|
|
response.text = 'The T Virus has been released. Beware of Zombies'
|
|
return response
|
|
else:
|
|
status = check_t_virus()
|
|
response.text = 'The T Virus is {0}'.format(status)
|
|
return response
|
|
|
|
.. note:: You do not need to explicitly allow the ``OPTIONS`` method; it is
|
|
always allowed. If an ``OPTIONS`` request is made, *Milla* will
|
|
automatically create a valid response informing the user of the allowed HTTP
|
|
request methods for the given request path. Your controller will not be
|
|
called in this case.
|
|
|
|
Controlling Access
|
|
==================
|
|
|
|
*Milla* provides a powerful and extensible authorization framework that can be
|
|
used to restrict access to different parts of a web application based on
|
|
properties of the request. This framework has two major components---request
|
|
validators and permission requirements. To use the framework, you must
|
|
implement a :term:`request validator` and then apply a :term:`permission
|
|
requirement` decorator to your :py:term:`controller` callables as needed.
|
|
|
|
Request Validators
|
|
******************
|
|
|
|
The default request validator (:py:class:`milla.auth.RequestValidator`) is
|
|
likely sufficient for most needs, as it assumes that a user is associated with
|
|
a request (via the ``user`` attribute on the :py:class:`~milla.Request` object)
|
|
and that the user has a ``permissions`` attribute that contains a list of
|
|
permissions the user holds.
|
|
|
|
.. note:: *Milla* does not automatically add a ``user`` attribute to
|
|
``Request`` instances, nor does it provide any way of determining what
|
|
permissions the user has. As such, you will need to handle both of these on
|
|
your own by utilizing the :ref:`before-after-hooks`.
|
|
|
|
Request validators are classes that have a ``validate`` method that takes a
|
|
request and optionally a permission requirement. The ``validate`` method should
|
|
return ``None`` if the request meets the requirements or raise
|
|
:py:exc:`~milla.auth.NotAuthorized` (or a subclass thereof) if it does not.
|
|
This exception will be called as the controller instead of the actual
|
|
controller if the request is not valid.
|
|
|
|
If you'd like to customize the response to invalid requests or the default
|
|
request validator is otherwise insufficient for your needs, you can create your
|
|
own request validator. To do this, you need to do the following:
|
|
|
|
1. Create a subclass of :py:class:`~milla.auth.RequestValidator` that overrides
|
|
:py:meth:`~milla.auth.RequestValidator.validate` method (taking care to
|
|
return ``None`` for valid requests and raise a subclass of
|
|
:py:exc:`~milla.auth.NotAuthorized` for invalid requests)
|
|
2. Register the new request validator in the ``milla.request_validator`` entry
|
|
point group in your ``setup.py``
|
|
|
|
For example:
|
|
|
|
.. code-block:: python
|
|
|
|
setup(name='UmbrellaCorpWeb',
|
|
...
|
|
entry_points={
|
|
'milla.request_validator': [
|
|
'html_login = umbrellacorpweb.lib:RequestValidatorLogin'
|
|
],
|
|
},
|
|
)
|
|
3. Set the ``request_validator`` application config key to the entry point name
|
|
of the new request validator
|
|
|
|
For example:
|
|
|
|
.. code-block:: python
|
|
|
|
application = milla.Application(Root())
|
|
application.config['request_validator'] = 'html_login'
|
|
|
|
Permission Requirements
|
|
***********************
|
|
|
|
Permission requirements are used by request validators to check whether or not
|
|
a request is authorized for a particular controller. Permission requirements
|
|
are applied to controller callables by using the
|
|
:py:meth:`~milla.auth.decorators.require_perms` decorator.
|
|
|
|
.. code-block:: python
|
|
|
|
class Root(object):
|
|
|
|
def __call__(self, request):
|
|
return 'This controller requires no permission'
|
|
|
|
@milla.require_perms('priority1')
|
|
def special(self, request):
|
|
return 'This controller requires Priority 1 permission'
|
|
|
|
You can specify advanced permission requirements by using
|
|
:py:class:`~milla.auth.permissions.Permission` objects:
|
|
|
|
.. code-block:: python
|
|
|
|
class Root(object):
|
|
|
|
def __call__(self, request):
|
|
return 'This controller requires no permission'
|
|
|
|
@milla.require_perms(Permission('priority1') | Permission('alpha2'))
|
|
def special(self, request):
|
|
return 'This controller requires Priority 1 or Alpha 2 permission'
|
|
|
|
Example
|
|
*******
|
|
|
|
The following example will demonstrate how to define a custom request validator
|
|
that presents an HTML form to the user for failed requests, allowing them to
|
|
log in:
|
|
|
|
``setup.py``:
|
|
|
|
.. code-block:: python
|
|
|
|
from setuptools import setup
|
|
|
|
setup(name='MyMillaApp',
|
|
version='1.0',
|
|
install_requires='Milla',
|
|
py_modules=['mymillaapp'],
|
|
entry_points={
|
|
'milla.request_validator': [
|
|
'html_login = mymillaapp:RequestValidatorLogin',
|
|
],
|
|
},
|
|
)
|
|
|
|
``mymillaapp.py``:
|
|
|
|
.. code-block:: python
|
|
|
|
import milla
|
|
import milla.auth
|
|
|
|
class NotAuthorizedLogin(milla.auth.NotAuthorized):
|
|
|
|
def __call__(self, request):
|
|
response = request.ResponseClass()
|
|
response.text = '''\
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<title>Please Log In</title>
|
|
<meta charset="UTF-8">
|
|
</head>
|
|
<body>
|
|
<h1>Please Log In</h1>
|
|
<div style="color: #ff0000;">{error}</div>
|
|
<form action="login" method="post">
|
|
<div>Username:</div>
|
|
<div><input type="text" name="username"></div>
|
|
<div>Password:</div>
|
|
<div><input type="password" name="password"></div>
|
|
<div><button type="submit">Submit</button></div>
|
|
</form>
|
|
</body>
|
|
</html>'''.format(error=self)
|
|
response.status_int = 401
|
|
response.headers['WWW-Authenticate'] = 'HTML-Form'
|
|
return response
|
|
|
|
class RequestValidatorLogin(milla.auth.RequestValidator):
|
|
|
|
exc_class = NotAuthorizedLogin
|
|
|
|
class Root(object):
|
|
|
|
def __before__(self, request):
|
|
# Actually determining the user from the request is beyond the
|
|
# scope of this example. You'll probably want to use a cookie-
|
|
# based session and a database for this.
|
|
request.user = get_user_from_request(request)
|
|
|
|
@milla.require_perms('kill_zombies')
|
|
def kill_zombies(self, request):
|
|
response = request.ResponseClass()
|
|
response.text = 'You can kill zombies'
|
|
return response
|
|
|
|
def __call__(self, request):
|
|
response = request.ResponseClass()
|
|
response.text = "Nothing to see here. No zombies, that's for sure"
|
|
return response
|
|
|
|
application = milla.Application(Root())
|
|
|
|
.. _HTTP 405: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6
|