Projet

Général

Profil

Télécharger (6,83 ko) Statistiques
| Branche: | Tag: | Révision:

root / entrouvert / djommon / multitenant / middleware.py @ 1b18c2d9

1 ec0613c1 Benjamin Dauvergne
import os
2
import json
3 aae80ef0 Benjamin Dauvergne
import glob
4 ec0613c1 Benjamin Dauvergne
5 e62d56fc Benjamin Dauvergne
from django.conf import settings, UserSettingsHolder
6 aae80ef0 Benjamin Dauvergne
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 e62d56fc Benjamin Dauvergne
11
SENTINEL = object()
12
13 aae80ef0 Benjamin Dauvergne
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
        schema = cls.hostname2schema(hostname)
37
        p = os.path.join(cls.base(), schema)
38
        if not os.path.exists(p):
39
            raise TenantNotFound
40
        return get_tenant_model()(schema_name=schema, domain_url=hostname)
41
42
    @classmethod
43
    def get_tenants(cls):
44
        self = cls()
45
        for path in glob.glob(os.path.join(cls.base(), '*')):
46
            hostname = os.path.basename(path)
47
            yield get_tenant_model()(
48
                    schema_name=self.hostname2schema(hostname),
49
                    domain_url=hostname)
50 e62d56fc Benjamin Dauvergne
51
    def process_request(self, request):
52 aae80ef0 Benjamin Dauvergne
        # connection needs first to be at the public schema, as this is where the
53
        # tenant informations are saved
54
        connection.set_schema_to_public()
55
        hostname_without_port = remove_www_and_dev(request.get_host().split(':')[0])
56
57
        try:
58
            request.tenant = self.get_tenant_by_hostname(hostname_without_port)
59
        except TenantNotFound:
60
            raise Http404
61
        connection.set_tenant(request.tenant)
62
63
        # content type can no longer be cached as public and tenant schemas have different
64
        # models. if someone wants to change this, the cache needs to be separated between
65
        # public and shared schemas. if this cache isn't cleared, this can cause permission
66
        # problems. for example, on public, a particular model has id 14, but on the tenants
67
        # it has the id 15. if 14 is cached instead of 15, the permissions for the wrong
68
        # model will be fetched.
69
        ContentType.objects.clear_cache()
70
71
        # do we have a public-specific token?
72
        if hasattr(settings, 'PUBLIC_SCHEMA_URLCONF') and request.tenant.schema_name == get_public_schema_name():
73
            request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
74
75
76
77 ec0613c1 Benjamin Dauvergne
78
class TenantSettingBaseMiddleware(object):
79
    '''Base middleware classe for loading settings based on tenants
80
81
       Child classes MUST override the load_tenant_settings() method.
82
    '''
83
    def __init__(self, *args, **kwargs):
84
        self.tenants_settings = {}
85
86
    def get_tenant_settings(self, wrapped, tenant):
87
        '''Get last loaded settings for tenant, try to update it by loading
88
           settings again is last loading time is less recent thant settings data
89
           store. Compare with last modification time is done in the
90
           load_tenant_settings() method.
91
        '''
92
        tenant_settings, last_time = self.tenants_settings.get(tenant.schema_name, (None,None))
93
        if tenant_settings is None:
94
            tenant_settings = UserSettingsHolder(wrapped)
95
        tenant_settings, last_time = self.load_tenant_settings(wrapped, tenant, tenant_settings, last_time)
96
        self.tenants_settings[tenant.schema_name] = tenant_settings, last_time
97
        return tenant_settings
98
99
    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
100
        '''Load tenant settings into tenant_settings object, eventually skip if
101
           last_time is more recent than last update time for settings and return
102
           the new value for tenant_settings and last_time'''
103
        raise NotImplemented
104
105
    def process_request(self, request):
106
        if not hasattr(request, '_old_settings_wrapped'):
107
            request._old_settings_wrapped = []
108
        request._old_settings_wrapped.append(settings._wrapped)
109
        settings._wrapped = self.get_tenant_settings(settings._wrapped, request.tenant)
110
111
    def process_response(self, request, response):
112
        if hasattr(request, '_old_settings_wrapped') and request._old_settings_wrapped:
113
            settings._wrapped = request._old_settings_wrapped.pop()
114
        return response
115
116
117
class FileBasedTenantSettingBaseMiddleware(TenantSettingBaseMiddleware):
118
    FILENAME = None
119
120
    def load_tenant_settings(self, wrapped, tenant, tenant_settings, last_time):
121
        path = os.path.join(settings.TENANT_BASE, tenant.schema_name, self.FILENAME)
122
        try:
123
            new_time = os.stat(path).st_mtime
124
        except OSError:
125
            # file was removed
126
            if not last_time is None:
127
                return UserSettingsHolder(wrapped), None
128
        else:
129
            if last_time is None or new_time >= last_time:
130
                # file is new
131
                tenant_settings = UserSettingsHolder(wrapped)
132
                self.load_file(tenant_settings, path)
133
                return tenant_settings, new_time
134
        # nothing has changed
135
        return tenant_settings, last_time
136
137
138
class JSONSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
139
    '''Load settings from a JSON file whose path is given by:
140
141
            os.path.join(settings.TENANT_BASE % schema_name, 'settings.json')
142
143
       The JSON file must be a dictionnary whose key/value will override
144
       current settings.
145
    '''
146
    FILENAME = 'settings.json'
147
148
    def load_file(sef, tenant_settings, path):
149
        with file(path) as f:
150
            json_settings = json.load(f)
151
            for key in json_settings:
152
                setattr(tenant_settings, key, json_settings[key])
153
154
155
class DictAdapter(dict):
156
    '''Give dict interface to plain objects'''
157
    def __init__(self, wrapped):
158
        self.wrapped = wrapped
159
160
    def __setitem__(self, key, value):
161
        setattr(self.wrapped, key, value)
162
163
    def __getitem__(self, key):
164
        try:
165
            return getattr(self.wrapped, key)
166
        except AttributeError:
167
            raise KeyError
168
169
170 1b18c2d9 Jérôme Schneider
class PythonSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
171 ec0613c1 Benjamin Dauvergne
    '''Load settings from a file whose path is given by:
172
173
            os.path.join(settings.TENANT_BASE % schema_name, 'settings.py')
174
175
       The file is executed in the same context as the classic settings file
176
       using execfile.
177
    '''
178
    FILENAME = 'settings.py'
179
180
    def load_file(self, tenant_settings, path):
181
        execfile(path, DictAdapter(tenant_settings))