Skip to content

Commit

Permalink
Merge pull request #452 from Jorl17/master
Browse files Browse the repository at this point in the history
Added BaseTenantMiddleware for easier custom middleware support. Thanks @Jorl17.
  • Loading branch information
bernardopires committed Apr 23, 2017
2 parents a4fb6cd + 2630ecb commit 9dd3858
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 14 deletions.
61 changes: 61 additions & 0 deletions docs/advanced_usage.rst
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

# 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']
Expand Down
3 changes: 2 additions & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ Contents

.. toctree::
:maxdepth: 2

install
use
advanced_usage
examples
templates
test
Expand Down
2 changes: 2 additions & 0 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 36 additions & 12 deletions tenant_schemas/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,54 @@
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.
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

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.
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).
"""
TENANT_NOT_FOUND_EXCEPTION = Http404
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
Expand All @@ -59,6 +75,14 @@ def process_request(self, request):
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):
"""
Expand Down

0 comments on commit 9dd3858

Please sign in to comment.