84 lines
3.3 KiB
Python
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
|