summaryrefslogtreecommitdiffstats
path: root/hobo/multitenant/middleware.py
blob: a855545386ffb15d972fd10bab60a216fc6736d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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