Project

General

Profile

Download (6.82 KB) Statistics
| Branch: | Tag: | Revision:

root / entrouvert / djommon / multitenant / middleware.py @ dc463f1d

1
import os
2
import json
3
import glob
4

    
5
from django.conf import settings, UserSettingsHolder
6
from django.db import connection
7
from django.http import Http404
8
from django.contrib.contenttypes.models import ContentType
9
from tenant_schemas.utils import get_tenant_model, remove_www_and_dev, get_public_schema_name
10

    
11
SENTINEL = object()
12

    
13
class TenantNotFound(RuntimeError):
14
    pass
15

    
16
class TenantMiddleware(object):
17
    """
18
    This middleware should be placed at the very top of the middleware stack.
19
    Selects the proper database schema using the request host. Can fail in
20
    various ways which is better than corrupting or revealing data...
21
    """
22
    @classmethod
23
    def base(cls):
24
        return settings.TENANT_BASE
25

    
26
    @classmethod
27
    def hostname2schema(cls, hostname):
28
        '''Convert hostname to PostgreSQL schema name'''
29
        if hostname in getattr(settings, 'TENANT_MAPPING', {}):
30
            return settings.TENANT_MAPPING[hostname]
31
        return hostname.replace('.', '_').replace('-', '_')
32

    
33
    @classmethod
34
    def get_tenant_by_hostname(cls, hostname):
35
        '''Retrieve a tenant object for this hostname'''
36
        if not os.path.exists(os.path.join(cls.base(), hostname)):
37
            raise TenantNotFound
38
        schema = cls.hostname2schema(hostname)
39
        return get_tenant_model()(schema_name=schema, domain_url=hostname)
40

    
41
    @classmethod
42
    def get_tenants(cls):
43
        self = cls()
44
        for path in glob.glob(os.path.join(cls.base(), '*')):
45
            hostname = os.path.basename(path)
46
            yield get_tenant_model()(
47
                    schema_name=self.hostname2schema(hostname),
48
                    domain_url=hostname)
49

    
50
    def process_request(self, request):
51
        # connection needs first to be at the public schema, as this is where the
52
        # tenant informations are saved
53
        connection.set_schema_to_public()
54
        hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0])
55

    
56
        try:
57
            request.tenant = self.get_tenant_by_hostname(hostname_without_port)
58
        except TenantNotFound:
59
            raise Http404
60
        connection.set_tenant(request.tenant)
61

    
62
        # content type can no longer be cached as public and tenant schemas have different
63
        # models. if someone wants to change this, the cache needs to be separated between
64
        # public and shared schemas. if this cache isn't cleared, this can cause permission
65
        # problems. for example, on public, a particular model has id 14, but on the tenants
66
        # it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
67
        # model will be fetched.
68
        ContentType.objects.clear_cache()
69

    
70
        # do we have a public-specific token?
71
        if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
72
            request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
73

    
74

    
75

    
76

    
77
class TenantSettingBaseMiddleware(object):
78
    '''Base middleware classe for loading settings based on tenants
79

    
80
       Child classes MUST override the load_tenant_settings() method.
81
    '''
82
    def __init__(self, *args, **kwargs):
83
        self.tenants_settings = {}
84

    
85
    def get_tenant_settings(self, wrapped, tenant):
86
        '''Get last loaded settings for tenant, try to update it by loading
87
           settings again is last loading time is less recent thant settings data
88
           store. Compare with last modification time is done in the
89
           load_tenant_settings() method.
90
        '''
91
        tenant_settings, last_time = self.tenants_settings.get(tenant.schema_name, (None,None))
92
        if tenant_settings is None:
93
            tenant_settings = UserSettingsHolder(wrapped)
94
        tenant_settings, last_time = self.load_tenant_settings(wrapped, tenant, tenant_settings, last_time)
95
        self.tenants_settings[tenant.schema_name] = tenant_settings, last_time
96
        return tenant_settings
97

    
98
    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
99
        '''Load tenant settings into tenant_settings object, eventually skip if
100
           last_time is more recent than last update time for settings and return
101
           the new value for tenant_settings and last_time'''
102
        raise NotImplemented
103

    
104
    def process_request(self, request):
105
        if not hasattr(request, '_old_settings_wrapped'):
106
            request._old_settings_wrapped = []
107
        request._old_settings_wrapped.append(settings._wrapped)
108
        settings._wrapped = self.get_tenant_settings(settings._wrapped, request.tenant)
109

    
110
    def process_response(self, request, response):
111
        if hasattr(request, '_old_settings_wrapped') and request._old_settings_wrapped:
112
            settings._wrapped = request._old_settings_wrapped.pop()
113
        return response
114

    
115

    
116
class FileBasedTenantSettingBaseMiddleware(TenantSettingBaseMiddleware):
117
    FILENAME = None
118

    
119
    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
120
        path = os.path.join(settings.TENANT_BASE, tenant.domain_url, self.FILENAME)
121
        try:
122
            new_time = os.stat(path).st_mtime
123
        except OSError:
124
            # file was removed
125
            if not last_time is None:
126
                return UserSettingsHolder(wrapped), None
127
        else:
128
            if last_time is None or new_time >= last_time:
129
                # file is new
130
                tenant_settings = UserSettingsHolder(wrapped)
131
                self.load_file(tenant_settings, path)
132
                return tenant_settings, new_time
133
        # nothing has changed
134
        return tenant_settings, last_time
135

    
136

    
137
class JSONSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
138
    '''Load settings from a JSON file whose path is given by:
139

    
140
            os.path.join(settings.TENANT_BASE % schema_name, 'settings.json')
141

    
142
       The JSON file must be a dictionnary whose key/value will override
143
       current settings.
144
    '''
145
    FILENAME = 'settings.json'
146

    
147
    def load_file(sef, tenant_settings, path):
148
        with file(path) as f:
149
            json_settings = json.load(f)
150
            for key in json_settings:
151
                setattr(tenant_settings, key, json_settings[key])
152

    
153

    
154
class DictAdapter(dict):
155
    '''Give dict interface to plain objects'''
156
    def __init__(self, wrapped):
157
        self.wrapped = wrapped
158

    
159
    def __setitem__(self, key, value):
160
        setattr(self.wrapped, key, value)
161

    
162
    def __getitem__(self, key):
163
        try:
164
            return getattr(self.wrapped, key)
165
        except AttributeError:
166
            raise KeyError
167

    
168

    
169
class PythonSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
170
    '''Load settings from a file whose path is given by:
171

    
172
            os.path.join(settings.TENANT_BASE % schema_name, 'settings.py')
173

    
174
       The file is executed in the same context as the classic settings file
175
       using execfile.
176
    '''
177
    FILENAME = 'settings.py'
178

    
179
    def load_file(self, tenant_settings, path):
180
        execfile(path, DictAdapter(tenant_settings))
(3-3/8)