hobo/hobo/multitenant/middleware.py

84 lines
3.3 KiB
Python

import os
import glob
import hashlib
from django.utils.encoding import smart_bytes
from django.conf import settings
from django.db import connection
from django.http import Http404, HttpResponseRedirect
from django.contrib.contenttypes.models import ContentType
from tenant_schemas.utils import get_tenant_model, get_public_schema_name
SENTINEL = object()
class TenantNotFound(RuntimeError):
pass
class TenantMiddleware(object):
"""
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...
"""
@classmethod
def base(cls):
return settings.TENANT_BASE
@classmethod
def hostname2schema(cls, hostname):
'''Convert hostname to PostgreSQL schema name'''
if hostname in getattr(settings, 'TENANT_MAPPING', {}):
return settings.TENANT_MAPPING[hostname]
schema = hostname.replace('.', '_').replace('-', '_')
if len(schema) > 63:
digest = hashlib.md5(smart_bytes(schema)).hexdigest()[:4]
schema = '%s_%s_%s' % (schema[:29], digest, schema[-28:])
return schema
@classmethod
def get_tenant_by_hostname(cls, hostname):
'''Retrieve a tenant object for this hostname'''
if not os.path.exists(os.path.join(cls.base(), hostname)):
raise TenantNotFound
schema = cls.hostname2schema(hostname)
return get_tenant_model()(schema_name=schema, domain_url=hostname)
@classmethod
def get_tenants(cls):
self = cls()
for path in glob.glob(os.path.join(cls.base(), '*')):
hostname = os.path.basename(path)
if hostname.endswith('.invalid'):
continue
if not os.path.isdir(path):
continue
yield get_tenant_model()(
schema_name=self.hostname2schema(hostname),
domain_url=hostname)
def process_request(self, request):
# connection needs first to be at the public schema, as this is where the
# tenant informations are saved
connection.set_schema_to_public()
hostname_without_port = request.get_host().split(':')[0]
try:
request.tenant = self.get_tenant_by_hostname(hostname_without_port)
except TenantNotFound:
if getattr(settings, 'TENANT_NOT_FOUND_REDIRECT_URL', None):
return HttpResponseRedirect(settings.TENANT_NOT_FOUND_REDIRECT_URL)
raise Http404
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 needs to be separated between
# public and shared schemas. if this cache isn't cleared, this can cause permission
# problems. for example, on public, a particular model has id 14, but on the tenants
# it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
# model will be fetched.
ContentType.objects.clear_cache()
# do we have a public-specific token?
if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF