Merge pull request #452 from Jorl17/master
Added BaseTenantMiddleware for easier custom middleware support. Thanks @Jorl17.
This commit is contained in:
commit
9dd3858dae
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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']
|
||||
|
|
|
@ -48,9 +48,10 @@ Contents
|
|||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
install
|
||||
use
|
||||
advanced_usage
|
||||
examples
|
||||
templates
|
||||
test
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue