Beispiel #1
0
    def setUpClass(cls):
        super(SharedAuthTest, cls).setUpClass()
        settings.SHARED_APPS = (
            'wagtailtenant',
            'django.contrib.auth',
            'django.contrib.contenttypes',
        )
        settings.TENANT_APPS = ('dts_test_app', )
        settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
        cls.sync_shared()
        Tenant(domain_url='test.com',
               schema_name=get_public_schema_name()).save(
                   verbosity=cls.get_verbosity())

        # Create a tenant
        cls.tenant = Tenant(domain_url='tenant.test.com', schema_name='tenant')
        cls.tenant.save(verbosity=cls.get_verbosity())

        # Create some users
        with schema_context(get_public_schema_name(
        )):  # this could actually also be executed inside a tenant
            cls.user1 = User(username='******', email="*****@*****.**")
            cls.user1.save()
            cls.user2 = User(username='******', email="*****@*****.**")
            cls.user2.save()

        # Create instances on the tenant that point to the users on public
        with tenant_context(cls.tenant):
            cls.d1 = ModelWithFkToPublicUser(user=cls.user1)
            cls.d1.save()
            cls.d2 = ModelWithFkToPublicUser(user=cls.user2)
            cls.d2.save()
Beispiel #2
0
 def set_schema_to_public(self):
     """
     Instructs to stay in the common 'public' schema.
     """
     self.tenant = FakeTenant(schema_name=get_public_schema_name())
     self.schema_name = get_public_schema_name()
     self.set_settings_schema(self.schema_name)
     self.search_path_set = False
Beispiel #3
0
    def handle(self, *args, **options):
        self.sync_tenant = options.get('tenant')
        self.sync_public = options.get('shared')
        self.schema_name = options.get('schema_name')
        self.executor = options.get('executor')
        self.installed_apps = settings.INSTALLED_APPS
        self.args = args
        self.options = options

        if self.schema_name:
            if self.sync_public:
                raise CommandError(
                    "schema should only be used with the --tenant switch.")
            elif self.schema_name == get_public_schema_name():
                self.sync_public = True
            else:
                self.sync_tenant = True
        elif not self.sync_public and not self.sync_tenant:
            # no options set, sync both
            self.sync_tenant = True
            self.sync_public = True

        if hasattr(settings, 'TENANT_APPS'):
            self.tenant_apps = settings.TENANT_APPS
        if hasattr(settings, 'SHARED_APPS'):
            self.shared_apps = settings.SHARED_APPS
Beispiel #4
0
    def test_command(self):
        """
        Tests that tenant_command is capable of wrapping commands
        and its parameters.
        """
        settings.SHARED_APPS = (
            'wagtailtenant',
            'django.contrib.contenttypes',
        )
        settings.TENANT_APPS = ()
        settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
        self.sync_shared()
        Tenant(
            domain_url='localhost',
            schema_name='public').save(verbosity=BaseTestCase.get_verbosity())

        out = StringIO()
        call_command('tenant_command',
                     args=('dumpdata', 'wagtailtenant'),
                     natural_foreign=True,
                     schema_name=get_public_schema_name(),
                     stdout=out)
        self.assertEqual(
            json.loads(
                '[{"fields": {"domain_url": "localhost", "schema_name": "public"}, '
                '"model": "wagtailtenant.tenant", "pk": 1}]'),
            json.loads(out.getvalue()))
Beispiel #5
0
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        # the imports below need to be done here else django <1.5 goes crazy
        # https://code.djangoproject.com/ticket/20704
        from django.db import connection
        from wagtail.wagtailtenant.utils import get_public_schema_name, app_labels
        from wagtail.wagtailtenant.postgresql_backend.base import DatabaseWrapper as TenantDbWrapper

        db_engine = settings.DATABASES[db]['ENGINE']
        if not (db_engine == 'wagtailtenant.postgresql_backend' or issubclass(
                getattr(load_backend(db_engine), 'DatabaseWrapper'),
                TenantDbWrapper)):
            return None

        if isinstance(app_label, ModelBase):
            # In django <1.7 the `app_label` parameter is actually `model`
            app_label = app_label._meta.app_label

        if connection.schema_name == get_public_schema_name():
            if app_label not in app_labels(settings.SHARED_APPS):
                return False
        else:
            if app_label not in app_labels(settings.TENANT_APPS):
                return False

        return None
Beispiel #6
0
    def test_tenant_apps_and_shared_apps_can_have_the_same_apps(self):
        """
        Tests that both SHARED_APPS and TENANT_APPS can have apps in common.
        In this case they should get synced to both tenant and public schemas.
        """
        settings.SHARED_APPS = (
            'wagtailtenant',  # 2 tables
            'django.contrib.auth',  # 6 tables
            'django.contrib.contenttypes',  # 1 table
            'django.contrib.sessions',
        )  # 1 table
        settings.TENANT_APPS = ('django.contrib.sessions', )  # 1 table
        settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
        self.sync_shared()
        tenant = Tenant(domain_url='arbitrary.test.com', schema_name='test')
        tenant.save(verbosity=BaseTestCase.get_verbosity())

        shared_tables = self.get_tables_list_in_schema(
            get_public_schema_name())
        tenant_tables = self.get_tables_list_in_schema(tenant.schema_name)
        self.assertEqual(2 + 6 + 1 + 1 + self.MIGRATION_TABLE_SIZE,
                         len(shared_tables))
        self.assertIn('django_session', shared_tables)
        self.assertEqual(1 + self.MIGRATION_TABLE_SIZE, len(tenant_tables))
        self.assertIn('django_session', tenant_tables)
Beispiel #7
0
    def run_migrations(self, tenants):
        public_schema_name = get_public_schema_name()

        if public_schema_name in tenants:
            run_migrations(self.args, self.options, self.codename, public_schema_name)
            tenants.pop(tenants.index(public_schema_name))

        self.run_tenant_migrations(tenants)
Beispiel #8
0
 def setUpClass(cls):
     super(RoutesTestCase, cls).setUpClass()
     settings.SHARED_APPS = ('wagtailtenant', )
     settings.TENANT_APPS = ('dts_test_app',
                             'django.contrib.contenttypes',
                             'django.contrib.auth', )
     settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
     cls.sync_shared()
     cls.public_tenant = Tenant(domain_url='test.com', schema_name=get_public_schema_name())
     cls.public_tenant.save(verbosity=BaseTestCase.get_verbosity())
Beispiel #9
0
    def get_tenant(self, model, hostname, request):
        try:
            return super(DefaultTenantMiddleware,
                         self).get_tenant(model, hostname, request)
        except model.DoesNotExist:
            schema_name = self.DEFAULT_SCHEMA_NAME
            if not schema_name:
                schema_name = get_public_schema_name()

            return model.objects.get(schema_name=schema_name)
Beispiel #10
0
    def setUpClass(cls):
        settings.TENANT_MODEL = 'wagtailtenant.Tenant'
        settings.SHARED_APPS = ('wagtailtenant', )
        settings.TENANT_APPS = (
            'dts_test_app',
            'django.contrib.contenttypes',
            'django.contrib.auth',
        )
        settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
        if '.test.com' not in settings.ALLOWED_HOSTS:
            settings.ALLOWED_HOSTS += ['.test.com']

        # Django calls syncdb by default for the test database, but we want
        # a blank public schema for this set of tests.
        connection.set_schema_to_public()
        cursor = connection.cursor()
        cursor.execute('DROP SCHEMA IF EXISTS %s CASCADE; CREATE SCHEMA %s;' %
                       (get_public_schema_name(), get_public_schema_name()))
        super(BaseTestCase, cls).setUpClass()
Beispiel #11
0
    def _cursor(self, name=None):
        """
        Here it happens. We hope every Django db operation using PostgreSQL
        must go through this to get the cursor handle. We change the path.
        """
        if name:
            # Only supported and required by Django 1.11 (server-side cursor)
            cursor = super(DatabaseWrapper, self)._cursor(name=name)
        else:
            cursor = super(DatabaseWrapper, self)._cursor()

        # optionally limit the number of executions - under load, the execution
        # of `set search_path` can be quite time consuming
        if (not get_limit_set_calls()) or not self.search_path_set:
            # Actual search_path modification for the cursor. Database will
            # search schemata from left to right when looking for the object
            # (table, index, sequence, etc.).
            if not self.schema_name:
                raise ImproperlyConfigured("Database schema not set. Did you forget "
                                           "to call set_schema() or set_tenant()?")
            _check_schema_name(self.schema_name)
            public_schema_name = get_public_schema_name()
            search_paths = []

            if self.schema_name == public_schema_name:
                search_paths = [public_schema_name]
            elif self.include_public_schema:
                search_paths = [self.schema_name, public_schema_name]
            else:
                search_paths = [self.schema_name]

            search_paths.extend(EXTRA_SEARCH_PATHS)

            if name:
                # Named cursor can only be used once
                cursor_for_search_path = self.connection.cursor()
            else:
                # Reuse
                cursor_for_search_path = cursor

            # In the event that an error already happened in this transaction and we are going
            # to rollback we should just ignore database error when setting the search_path
            # if the next instruction is not a rollback it will just fail also, so
            # we do not have to worry that it's not the good one
            try:
                cursor_for_search_path.execute('SET search_path = {0}'.format(','.join(search_paths)))
            except (django.db.utils.DatabaseError, psycopg2.InternalError):
                self.search_path_set = False
            else:
                self.search_path_set = True

            if name:
                cursor_for_search_path.close()

        return cursor
Beispiel #12
0
    def handle(self, *args, **options):
        super(Command, self).handle(*args, **options)
        self.PUBLIC_SCHEMA_NAME = get_public_schema_name()

        executor = get_executor(codename=self.executor)(self.args,
                                                        self.options)

        if self.sync_public and not self.schema_name:
            self.schema_name = self.PUBLIC_SCHEMA_NAME

        if self.sync_public:
            executor.run_migrations(tenants=[self.schema_name])

        if self.sync_tenant:
            if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME:
                if not schema_exists(self.schema_name):
                    raise MigrationSchemaMissing(
                        'Schema "{}" does not exist'.format(self.schema_name))
                else:
                    tenants = [self.schema_name]
            else:
                tenants = get_tenant_model().objects.exclude(
                    schema_name=get_public_schema_name()).values_list(
                        'schema_name', flat=True)

            executor.run_migrations(tenants=tenants)

            from wagtail.wagtailcustomers.models import Customer as customer
            if not customer.objects.filter(
                    schema_name=get_public_schema_name()).exists():
                customer(domain_url='tuiuiu.io',
                         schema_name=get_public_schema_name(),
                         name='tuiuiu.io',
                         description='tuiuiu.io').save()

            from django.contrib.auth.models import User as user
            if not user.objects.filter(username='******').exists():
                user.objects.create_superuser('admin', '*****@*****.**',
                                              'admin')
Beispiel #13
0
    def save(self, verbosity=1, *args, **kwargs):
        is_new = self.pk is None

        if is_new and connection.schema_name != get_public_schema_name():
            raise Exception("Can't create tenant outside the public schema. "
                            "Current schema is %s." % connection.schema_name)
        elif not is_new and connection.schema_name not in (
                self.schema_name, get_public_schema_name()):
            raise Exception("Can't update tenant outside it's own schema or "
                            "the public schema. Current schema is %s." %
                            connection.schema_name)

        super(TenantMixin, self).save(*args, **kwargs)

        if is_new and self.auto_create_schema:
            try:
                self.create_schema(check_if_exists=True, verbosity=verbosity)
            except:
                # We failed creating the tenant, delete what we created and
                # re-raise the exception
                self.delete(force_drop=True)
                raise
            else:
                post_schema_sync.send(sender=TenantMixin, tenant=self)
Beispiel #14
0
 def handle(self, *args, **options):
     """
     Iterates a command over all registered schemata.
     """
     if options['schema_name']:
         # only run on a particular schema
         connection.set_schema_to_public()
         self.execute_command(
             get_tenant_model().objects.get(
                 schema_name=options['schema_name']), self.COMMAND_NAME,
             *args, **options)
     else:
         for tenant in get_tenant_model().objects.all():
             if not (options['skip_public']
                     and tenant.schema_name == get_public_schema_name()):
                 self.execute_command(tenant, self.COMMAND_NAME, *args,
                                      **options)
Beispiel #15
0
    def delete(self, force_drop=False, *args, **kwargs):
        """
        Deletes this row. Drops the tenant's schema if the attribute
        auto_drop_schema set to True.
        """
        if connection.schema_name not in (self.schema_name,
                                          get_public_schema_name()):
            raise Exception("Can't delete tenant outside it's own schema or "
                            "the public schema. Current schema is %s." %
                            connection.schema_name)

        if schema_exists(self.schema_name) and (self.auto_drop_schema
                                                or force_drop):
            cursor = connection.cursor()
            cursor.execute('DROP SCHEMA IF EXISTS %s CASCADE' %
                           self.schema_name)

        return super(TenantMixin, self).delete(*args, **kwargs)
Beispiel #16
0
    def test_shared_apps_does_not_sync_tenant_apps(self):
        """
        Tests that if an app is in SHARED_APPS, it does not get synced to
        the a tenant schema.
        """
        settings.SHARED_APPS = (
            'wagtailtenant',  # 2 tables
            'django.contrib.auth',  # 6 tables
            'django.contrib.contenttypes',
        )  # 1 table
        settings.TENANT_APPS = ('django.contrib.sessions', )
        settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
        self.sync_shared()

        shared_tables = self.get_tables_list_in_schema(
            get_public_schema_name())
        self.assertEqual(2 + 6 + 1 + self.MIGRATION_TABLE_SIZE,
                         len(shared_tables))
        self.assertNotIn('django_session', shared_tables)
Beispiel #17
0
    def test_content_types_is_not_mandatory(self):
        """
        Tests that even if content types is in SHARED_APPS, it's
        not required in TENANT_APPS.
        """
        settings.SHARED_APPS = (
            'wagtailtenant',  # 2 tables
            'django.contrib.contenttypes',
        )  # 1 table
        settings.TENANT_APPS = ('django.contrib.sessions', )  # 1 table
        settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS
        self.sync_shared()
        tenant = Tenant(domain_url='something.test.com', schema_name='test')
        tenant.save(verbosity=BaseTestCase.get_verbosity())

        shared_tables = self.get_tables_list_in_schema(
            get_public_schema_name())
        tenant_tables = self.get_tables_list_in_schema(tenant.schema_name)
        self.assertEqual(2 + 1 + self.MIGRATION_TABLE_SIZE, len(shared_tables))
        self.assertIn('django_session', tenant_tables)
        self.assertEqual(1 + self.MIGRATION_TABLE_SIZE, len(tenant_tables))
        self.assertIn('django_session', tenant_tables)
Beispiel #18
0
    def process_request(self, request):
        # Connection needs first to be at the public schema, as this is where
        # the tenant metadata is stored.
        connection.set_schema_to_public()

        hostname = self.hostname_from_request(request)
        TenantModel = get_tenant_model()

        try:
            # get_tenant must be implemented by extending this class.
            tenant = self.get_tenant(TenantModel, hostname, request)
            assert isinstance(tenant, TenantModel)
        except TenantModel.DoesNotExist:
            raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for {!r}'.format(
                request.get_host()))
        except AssertionError:
            raise self.TENANT_NOT_FOUND_EXCEPTION('Invalid tenant {!r}'.format(
                request.tenant))

        request.tenant = tenant
        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 urlconf?
        if hasattr(
                settings, 'PUBLIC_SCHEMA_URLCONF'
        ) and request.tenant.schema_name == get_public_schema_name():
            request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
Beispiel #19
0
def best_practice(app_configs, **kwargs):
    """
    Test for configuration recommendations. These are best practices, they
    avoid hard to find bugs and unexpected behaviour.
    """
    if app_configs is None:
        app_configs = apps.get_app_configs()

    # Take the app_configs and turn them into *old style* application names.
    # This is what we expect in the SHARED_APPS and TENANT_APPS settings.
    INSTALLED_APPS = [
        config.name
        for config in app_configs
    ]

    if not hasattr(settings, 'TENANT_APPS'):
        return [Critical('TENANT_APPS setting not set')]

    if not hasattr(settings, 'TENANT_MODEL'):
        return [Critical('TENANT_MODEL setting not set')]

    if not hasattr(settings, 'SHARED_APPS'):
        return [Critical('SHARED_APPS setting not set')]

    if 'wagtail.wagtailtenant.routers.TenantSyncRouter' not in settings.DATABASE_ROUTERS:
        return [
            Critical("DATABASE_ROUTERS setting must contain "
                     "'wagtail.wagtailtenant.routers.TenantSyncRouter'.")
        ]

    errors = []

    django_index = next(i for i, s in enumerate(INSTALLED_APPS) if s.startswith('django.'))
    if INSTALLED_APPS.index('wagtail.wagtailtenant') > django_index:
        errors.append(
            Warning("You should put 'wagtail.wagtailtenant' before any django "
                    "core applications in INSTALLED_APPS.",
                    obj="django.conf.settings",
                    hint="This is necessary to overwrite built-in django "
                         "management commands with their schema-aware "
                         "implementations.",
                    id="wagtailtenant.W001"))

    if not settings.TENANT_APPS:
        errors.append(
            Error("TENANT_APPS is empty.",
                  hint="Maybe you don't need this app?",
                  id="wagtailtenant.E001"))

    if hasattr(settings, 'PG_EXTRA_SEARCH_PATHS'):
        if get_public_schema_name() in settings.PG_EXTRA_SEARCH_PATHS:
            errors.append(Critical(
                "%s can not be included on PG_EXTRA_SEARCH_PATHS."
                % get_public_schema_name()))

        # make sure no tenant schema is in settings.PG_EXTRA_SEARCH_PATHS
        invalid_schemas = set(settings.PG_EXTRA_SEARCH_PATHS).intersection(
            get_tenant_model().objects.all().values_list('schema_name', flat=True))
        if invalid_schemas:
            errors.append(Critical(
                "Do not include tenant schemas (%s) on PG_EXTRA_SEARCH_PATHS."
                % ", ".join(sorted(invalid_schemas))))

    if not settings.SHARED_APPS:
        errors.append(
            Warning("SHARED_APPS is empty.",
                    id="wagtailtenant.W002"))

    if not set(settings.TENANT_APPS).issubset(INSTALLED_APPS):
        delta = set(settings.TENANT_APPS).difference(INSTALLED_APPS)
        errors.append(
            Error("You have TENANT_APPS that are not in INSTALLED_APPS",
                  hint=[a for a in settings.TENANT_APPS if a in delta],
                  id="wagtailtenant.E002"))

    if not set(settings.SHARED_APPS).issubset(INSTALLED_APPS):
        delta = set(settings.SHARED_APPS).difference(INSTALLED_APPS)
        errors.append(
            Error("You have SHARED_APPS that are not in INSTALLED_APPS",
                  hint=[a for a in settings.SHARED_APPS if a in delta],
                  id="wagtailtenant.E003"))

    if not isinstance(default_storage, TenantStorageMixin):
        errors.append(Warning(
            "Your default storage engine is not tenant aware.",
            hint="Set settings.DEFAULT_FILE_STORAGE to "
                 "'wagtail.wagtailtenant.storage.TenantFileSystemStorage'",
            id="wagtailtenant.W003"
        ))

    return errors
Beispiel #20
0
 def sync_shared(cls):
     call_command('migrate_schemas',
                  schema_name=get_public_schema_name(),
                  interactive=False,
                  verbosity=cls.get_verbosity(),
                  run_syncdb=True)
Beispiel #21
0
 def sync_shared(cls):
     call_command('migrate_schemas',
                  schema_name=get_public_schema_name(),
                  interactive=False,
                  verbosity=0)