Advanced Features

Milla contains several powerful tools that allow web developers complete control over how their applications behave.

Propagating Configuration

While one possible way for 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 Application is created and will be available to controllers as the config attribute of the request.

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 read_config() that can produce a flat dictionary from a standard configuration file:

; umbrella.ini
[t_virus]
status = escaped
# 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 allow() decorator.

@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 request validator and then apply a permission requirement decorator to your controller callables as needed.

Request Validators

The default request validator (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 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 Before and 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 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 RequestValidator that overrides validate() method (taking care to return None for valid requests and raise a subclass of NotAuthorized for invalid requests)

  2. Register the new request validator in the milla.request_validator entry point group in your setup.py

    For example:

    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:

    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 require_perms() decorator.

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 Permission objects:

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:

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:

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