Projet

Général

Profil

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

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

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
        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

    
51
    def process_request(self, request):
52
        # 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

    
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
class PythonSettingsMiddleware(FileBasedTenantSettingBaseMiddleware):
171
    '''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))
(3-3/8)