Merge pull request #452 from Jorl17/master

Added BaseTenantMiddleware for easier custom middleware support. Thanks @Jorl17.
This commit is contained in:
Bernardo Pires 2017-04-23 23:00:35 +02:00 committed by GitHub
commit 9dd3858dae
5 changed files with 103 additions and 15 deletions

61
docs/advanced_usage.rst Normal file
View File

@ -0,0 +1,61 @@
==============
Advanced Usage
==============
Custom tenant strategies (custom middleware support)
====================================================
By default, ``django-tenant-schemas``'s strategies for determining the correct tenant involve extracting it from the URL (e.g. ``mytenant.mydomain.com``). This is done through a middleware, typically ``TenantMiddleware``.
In some situations, it might be useful to use **alternative tenant selection strategies**. For example, consider a website with a fixed URL. An approach for this website might be to pass the tenant through a special header, or to determine it in some other manner based on the request (e.g. using an OAuth token mapped to a tenant). ``django-tenant-schemas`` offer an **easily extensible way to provide your own middleware** with minimal code changes.
To add custom tenant selection strategies, you need to **subclass the** ``BaseTenantMiddleware`` **class and implement its** ``get_tenant`` **method**. This method accepts the current ``request`` object through which you can determine the tenant to use. In addition, for backwards-compatibility reasons, the method also accepts the tenant model class (``TENANT_MODEL``) and the ``hostname`` of the current request. **You should return an instance of your** ``TENANT_MODEL`` **class** from this function.
After creating your middleware, you should make it the top-most middleware in your list. You should only have one subclass of ``BaseTenantMiddleware`` per project.
Note that you might also wish to extend the other provided middleware classes, such as ``TenantMiddleware``. For example, you might want to chain several strategies together, and you could do so by subclassing the original strategies and manipulating the call to ``super``'s ``get_tenant``.
Example: Determine tenant from HTTP header
------------------------------------------
Suppose you wanted to determine the current tenant based on a request header (``X-DTS-SCHEMA``). You might implement a simple middleware such as:
.. code-block:: python
class XHeaderTenantMiddleware(BaseTenantMiddleware):
"""
Determines tenant by the value of the ``X-DTS-SCHEMA`` HTTP header.
"""
def get_tenant(self, model, hostname, request):
schema_name = request.META.get('HTTP_X_DTS_SCHEMA', get_public_schema_name())
return model.objects.get(schema_name=schema_name)
Your application could now specify the tenant with the ``X-DTS-SCHEMA`` HTTP header. In scenarios where you are configuring individual tenant websites by yourself, each with its own ``nginx`` configuration to redirect to the right tenant, you could use a configuration such as the one below:
.. code-block:: nginx
# /etc/nginx/conf.d/multitenant.conf
upstream web {
server localhost:8000;
}
server {
listen 80 default_server;
server_name _;
location / {
proxy_pass http://web;
proxy_set_header Host $host;
}
}
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://web;
proxy_set_header Host $host;
proxy_set_header X-DTS-SCHEMA example; # triggers XHeaderTenantMiddleware
}
}

View File

@ -22,7 +22,7 @@ import datetime
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.autosectionlabel',]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@ -48,9 +48,10 @@ Contents
.. toctree::
:maxdepth: 2
install
use
advanced_usage
examples
templates
test

View File

@ -38,6 +38,8 @@ If you'd like to raise ``DisallowedHost`` and a HTTP 400 response instead, use t
If you'd like to serve the public tenant for unrecognised hostnames instead, use ``tenant_schemas.middleware.DefaultTenantMiddleware``. To use a tenant other than the public tenant, create a subclass and register it instead.
If you'd like a different tenant selection technique (e.g. using an HTTP Header), you can define a custom middleware. See :ref:`Advanced Usage`.
.. code-block:: python
from tenant_schemas.middleware import DefaultTenantMiddleware

View File

@ -13,38 +13,54 @@ if django.VERSION >= (1, 10, 0):
else:
MIDDLEWARE_MIXIN = object
"""
These middlewares should be placed at the very top of the middleware stack.
Selects the proper database schema using request information. Can fail in
various ways which is better than corrupting or revealing data.
class TenantMiddleware(MIDDLEWARE_MIXIN):
"""
This middleware should be placed at the very top of the middleware stack.
Selects the proper database schema using the request host. Can fail in
various ways which is better than corrupting or revealing data.
"""
Extend BaseTenantMiddleware for a custom tenant selection strategy,
such as inspecting the header, or extracting it from some OAuth token.
"""
class BaseTenantMiddleware(MIDDLEWARE_MIXIN):
TENANT_NOT_FOUND_EXCEPTION = Http404
"""
Subclass and override this to achieve desired behaviour. Given a
request, return the tenant to use. Tenant should be an instance
of TENANT_MODEL. We have three parameters for backwards compatibility
(the request would be enough).
"""
def get_tenant(self, model, hostname, request):
raise NotImplementedError
def hostname_from_request(self, request):
""" Extracts hostname from request. Used for custom requests filtering.
By default removes the request's port and common prefixes.
"""
return remove_www(request.get_host().split(':')[0]).lower()
def get_tenant(self, model, hostname, request):
return model.objects.get(domain_url=hostname)
def process_request(self, request):
# Connection needs first to be at the public schema, as this is where
# the tenant metadata is stored.
connection.set_schema_to_public()
hostname = self.hostname_from_request(request)
hostname = self.hostname_from_request(request)
TenantModel = get_tenant_model()
try:
request.tenant = self.get_tenant(TenantModel, hostname, request)
connection.set_tenant(request.tenant)
# get_tenant must be implemented by extending this class.
tenant = self.get_tenant(TenantModel, hostname, request)
assert isinstance(tenant, TenantModel)
except TenantModel.DoesNotExist:
raise self.TENANT_NOT_FOUND_EXCEPTION(
'No tenant for hostname "%s"' % hostname)
'No tenant for {!r}'.format(request.get_host()))
except AssertionError:
raise self.TENANT_NOT_FOUND_EXCEPTION(
'Invalid tenant {!r}'.format(request.tenant))
request.tenant = tenant
connection.set_tenant(request.tenant)
# Content type can no longer be cached as public and tenant schemas
# have different models. If someone wants to change this, the cache
@ -59,6 +75,14 @@ class TenantMiddleware(MIDDLEWARE_MIXIN):
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
class TenantMiddleware(BaseTenantMiddleware):
"""
Selects the proper database schema using the request host. E.g. <my_tenant>.<my_domain>
"""
def get_tenant(self, model, hostname, request):
return model.objects.get(domain_url=hostname)
class SuspiciousTenantMiddleware(TenantMiddleware):
"""