def setUpClass(cls): super(SharedAuthTest, cls).setUpClass() settings.SHARED_APPS = ( 'tenant_schemas', '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()
def _get_tenant(self, request, allow_public=None): tenant_id = request.session.get('active_tenant') tenant = None TenantModel = get_tenant_model() if tenant_id is not None: try: tenant = TenantModel.objects.get(pk=tenant_id) except TenantModel.DoesNotExist: del request.session['active_tenant'] tenant = None if tenant is None: tenant = connection.tenant if tenant.schema_name == get_public_schema_name(): allow_public = allow_public if allow_public is not None else ( self.model._meta.app_label in app_labels(settings.TENANT_APPS)) if not allow_public: return None tenant = TenantModel.objects.exclude( schema_name=get_public_schema_name()).first() if tenant: self.message_user(request, "This model does not exist in public schema." " Falling into any first tenant.", level=messages.WARNING) return tenant
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)
def save(self, verbosity=1, *args, **kwargs): is_new = self.pk is None if not self.db_string: self.db_string = get_db_string(self.schema_name) db = self.db_string from django.db import connection if db: connection = connections[db] 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, db=db) except Exception as e: # We failed creating the tenant, delete what we created and # re-raise the exception self.delete(force_drop=True, using=db) raise else: post_schema_sync.send(sender=TenantMixin, tenant=self)
def setUpClass(cls): super(SharedAuthTest, cls).setUpClass() settings.SHARED_APPS = ('tenant_schemas', '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() # Create a tenant cls.tenant = Tenant(domain_url='tenant.test.com', schema_name='tenant') cls.tenant.save() # 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()
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)
def sync_shared(cls, db=None): if not db: call_command('migrate_schemas', schema_name=get_public_schema_name(), interactive=False, verbosity=cls.get_verbosity(), run_syncdb=True) call_command('migrate_schemas', schema_name=get_public_schema_name(), interactive=False, verbosity=cls.get_verbosity(), run_syncdb=True, db=db)
def sync_shared(cls): if django.VERSION >= (1, 7, 0): call_command('migrate_schemas', schema_name=get_public_schema_name(), interactive=False, verbosity=cls.get_verbosity()) else: call_command('sync_schemas', schema_name=get_public_schema_name(), tenant=False, public=True, interactive=False, migrate_all=True, verbosity=cls.get_verbosity())
def get_tenant(self, model, hostname, request): try: schema = model.objects.get(schema_name=get_public_schema_name()) except ObjectDoesNotExist as ex: schema = model.objects.create( domain_url=hostname, schema_name=get_public_schema_name(), tenant_name=TenantConstants.DEFAULT_TENANT_NAME) schema.save() authentication = request.META.get('HTTP_AUTHORIZATION') if authentication is not None and "Bearer " in authentication: tenant = JWTUtils.decode_access_token(authentication, False) tenant_name = tenant.get("tenant") schema = model.objects.get(tenant_name=tenant_name) return schema
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: self.create_schema(check_if_exists=True, verbosity=verbosity) post_schema_sync.send(sender=TenantMixin, tenant=self)
def get_tenant(self, model, hostname, request): try: public_schema = model.objects.get(schema_name=get_public_schema_name()) except ObjectDoesNotExist: public_schema = model.objects.create( domain_url=hostname, schema_name=get_public_schema_name(), tenant_name=get_public_schema_name().capitalize(), paid_until=date.today() + relativedelta(months=+1), on_trial=True) public_schema.save() x_request_id = request.META.get('HTTP_X_REQUEST_ID', public_schema.tenant_uuid) tenant_model = model.objects.get(tenant_uuid=x_request_id) print(tenant_model, public_schema) return tenant_model if not None else public_schema
def setUpClass(cls): settings.TENANT_MODEL = 'tenant_schemas.Tenant' settings.SHARED_APPS = ('tenant_schemas', ) settings.TENANT_APPS = ('dts_test_app', 'django.contrib.contenttypes', 'django.contrib.auth', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS # 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 %s CASCADE; CREATE SCHEMA %s;' % (get_public_schema_name(), get_public_schema_name(), )) super(BaseTestCase, cls).setUpClass()
def setUpClass(cls): settings.TENANT_MODEL = 'tenant_schemas.Tenant' settings.SHARED_APPS = ('tenant_schemas', ) settings.TENANT_APPS = ('dts_test_app', 'django.contrib.contenttypes', 'django.contrib.auth', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS # 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()
def sync_shared(cls): if django.VERSION >= (1, 7, 0): call_command('migrate_schemas', schema_name=get_public_schema_name(), interactive=False, verbosity=cls.get_verbosity(), run_syncdb=True) else: call_command('sync_schemas', schema_name=get_public_schema_name(), tenant=False, public=True, interactive=False, migrate_all=True, verbosity=cls.get_verbosity())
def handle(self, *args, **options): self.non_tenant_schemas = settings.PG_EXTRA_SEARCH_PATHS + ['public'] self.sync_tenant = options.get('tenant') self.sync_public = options.get('shared') self.schema_name = options.get('schema_name') self.args = args self.options = options self.PUBLIC_SCHEMA_NAME = get_public_schema_name() if self.schema_name: if self.sync_public: raise CommandError("schema should only be used " "with the --tenant switch.") elif self.schema_name == self.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 self.sync_public and not self.schema_name: self.schema_name = self.PUBLIC_SCHEMA_NAME if self.sync_public: self.run_migrations(self.schema_name, settings.SHARED_APPS) if self.sync_tenant: if self.schema_name and \ (self.schema_name != self.PUBLIC_SCHEMA_NAME): # Make sure the tenant exists and the schema belongs to # a tenant; We don't want to sync to extensions schema by # mistake if not schema_exists(self.schema_name): raise RuntimeError('Schema "{}" does not exist'.format( self.schema_name)) elif self.schema_name in self.non_tenant_schemas: raise RuntimeError( 'Schema "{}" does not belong to any tenant'.format( self.schema_name)) else: self.run_migrations(self.schema_name, settings.TENANT_APPS) else: all_tenants = get_tenant_model().objects.exclude( schema_name=get_public_schema_name()) for tenant in all_tenants: self.run_migrations(tenant.schema_name, settings.TENANT_APPS)
def handle_noargs(self, **options): # todo: awful lot of duplication from sync_schemas sync_tenant = options.get('tenant') sync_public = options.get('shared') schema_name = options.get('schema_name') self.installed_apps = settings.INSTALLED_APPS self.options = options if sync_public and schema_name: raise CommandError("schema should only be used with the --tenant switch.") if not hasattr(settings, 'TENANT_APPS') and sync_tenant: raise CommandError("No setting found for TENANT_APPS") if not hasattr(settings, 'SHARED_APPS') and sync_public: raise CommandError("No setting found for SHARED_APPS") if not sync_public and not sync_tenant and not schema_name: # no options set, sync both sync_tenant = True sync_public = True if schema_name: if schema_name == get_public_schema_name(): sync_public = True else: sync_tenant = True if hasattr(settings, 'TENANT_APPS'): self.tenant_apps = settings.TENANT_APPS if hasattr(settings, 'SHARED_APPS'): self.shared_apps = settings.SHARED_APPS if sync_public: self.migrate_public_apps() if sync_tenant: self.migrate_tenant_apps(schema_name)
def migrate_tenant_apps(self, schema_name=None): self._save_south_settings() apps = self.tenant_apps or self.installed_apps self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps) if schema_name: self._notice("=== Running migrate for schema: %s" % schema_name) connection.set_schema_to_public() tenant = get_tenant_model().objects.get(schema_name=schema_name) self._migrate_schema(tenant) else: all_tenants = get_tenant_model().objects.exclude( schema_name=get_public_schema_name()) if not all_tenants: self._notice("No tenants found") for tenant in all_tenants: Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache self._notice("=== Running migrate for schema %s" % tenant.schema_name) self._migrate_schema(tenant) self._restore_south_settings()
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 tenant_schemas.utils import get_public_schema_name, app_labels from tenant_schemas.postgresql_backend.base import DatabaseWrapper as TenantDbWrapper db_engine = settings.DATABASES[db]["ENGINE"] if not ( db_engine == "tenant_schemas.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
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
def delete(self, request, name=None): schemas = list(Tenant.objects.all().values_list('schema_name', flat=True)) schemas.remove(get_public_schema_name()) if name: try: probe = admin_models.Probe.objects.get(name=name) mt = admin_models.MetricTemplate.objects.filter( probekey=admin_models.ProbeHistory.objects.get( name=probe.name, package__version=probe.package.version)) if len(mt) == 0: for schema in schemas: # need to iterate through schemas because of foreign # key in Metric model with schema_context(schema): admin_models.ProbeHistory.objects.filter( object_id=probe).delete() probe.delete() return Response(status=status.HTTP_204_NO_CONTENT) else: return Response( { 'detail': 'You cannot delete Probe that is associated ' 'to metric templates!' }, status=status.HTTP_400_BAD_REQUEST) except admin_models.Probe.DoesNotExist: raise NotFound(status=404, detail='Probe not found') else: return Response(status=status.HTTP_400_BAD_REQUEST)
def migrate_tenant_apps(self, schema_name=None): self._save_south_settings() apps = self.tenant_apps or self.installed_apps self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps) migrate_command = MigrateCommand() if schema_name: print self.style.NOTICE("=== Running migrate for schema: %s" % schema_name) connection.set_schema_to_public() sync_tenant = get_tenant_model().objects.filter( schema_name=schema_name).get() connection.set_tenant(sync_tenant, include_public=False) migrate_command.execute(**self.options) else: public_schema_name = get_public_schema_name() tenant_schemas_count = get_tenant_model().objects.exclude( schema_name=public_schema_name).count() if not tenant_schemas_count: print self.style.NOTICE("No tenants found") for tenant_schema in get_tenant_model().objects.exclude( schema_name=public_schema_name).all(): Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache print self.style.NOTICE("=== Running migrate for schema %s" % tenant_schema.schema_name) connection.set_tenant(tenant_schema, include_public=False) migrate_command.execute(**self.options) self._restore_south_settings()
def process_request(self, request, *args, **kwargs): db = self.get_database(request) connections[db].set_schema_to_public() request_cfg.db = db 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 connections[db].set_tenant(request.tenant) # 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
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 = ( 'tenant_schemas', # 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)
def invite_to_company(self, request): if 'emp_id' in request.data: emp_id = request.data['emp_id'] else: return Response(data="Required parameter 'emp_id' is missing", status=status.HTTP_400_BAD_REQUEST) if 'company_from' in request.data: company_from = request.data['company_from'] else: return Response(data="Required parameter 'company_from' is missing", status=status.HTTP_400_BAD_REQUEST) if company_from == connection.schema_name: return Response(data='Cannot invite employee from same company.', status=status.HTTP_400_BAD_REQUEST) with schema_context(get_public_schema_name()): if not Company.objects.filter(name=company_from).exists(): return Response(data='Invalid company name.', status=status.HTTP_400_BAD_REQUEST) with schema_context(company_from): try: employee = Employee.objects.get(id=emp_id) except Employee.DoesNotExist: return Response(data='Invalid employee id.', status=status.HTTP_400_BAD_REQUEST) return Response(data='Employee invited to company.', status=status.HTTP_200_OK)
def _cursor(self): """ Here it happens. We hope every Django db operation using PostgreSQL must go through this to get the cursor handle. We change the path. """ cursor = super(DatabaseWrapper, self)._cursor() # 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_identifier(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) cursor.execute('SET search_path = {0}'.format(','.join(search_paths))) return cursor
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.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
def create_profile(sender, **kwargs): from django.db import connection if connection.schema_name != get_public_schema_name(): current_user = kwargs["instance"] if kwargs["created"]: new_profile = Profile(user=current_user) # if user's name matches student number (e.g 9912345), set student number: pattern = re.compile(Profile.student_number_regex_string) if pattern.match(current_user.get_username()): new_profile.student_number = int(current_user.get_username()) # set first and last name on the profile. This should be removed and just use the first and last name # from the user model! But when first implemented, first and last name weren't included in the the sign up form. new_profile.first_name = current_user.first_name new_profile.last_name = current_user.last_name new_profile.save() staff_list = User.objects.filter(is_staff=True) notify.send( current_user, target=new_profile, recipient=staff_list[0], affected_users=staff_list, icon="<i class='fa fa-fw fa-lg fa-user text-success'></i>", verb='. New user registered: ')
def delete(self, request, name=None): schemas = list(Tenant.objects.all().values_list('schema_name', flat=True)) schemas.remove(get_public_schema_name()) if name: try: mt = admin_models.MetricTemplate.objects.get(name=name) for schema in schemas: with schema_context(schema): try: admin_models.History.objects.filter( object_id=mt.id, content_type=ContentType.objects.get_for_model( mt)).delete() m = Metric.objects.get(name=name) TenantHistory.objects.filter( object_id=m.id, content_type=ContentType.objects.get_for_model( m)).delete() m.delete() except Metric.DoesNotExist: pass mt.delete() return Response(status=status.HTTP_204_NO_CONTENT) except admin_models.MetricTemplate.DoesNotExist: raise NotFound(status=404, detail='Metric template not found') else: return Response(status=status.HTTP_400_BAD_REQUEST)
def process_request(self, request): """ Resets to public schema Some nasty weird bugs happened at the production environment without this call. connection.pg_thread.schema_name would already be set and then terrible errors would occur. Any idea why? My theory is django implements connection as some sort of threading local variable. """ connection.set_schema_to_public() request.tenant = self.set_tenant(request.get_host()) # do we have tenant-specific URLs? if ( hasattr(settings, "PUBLIC_SCHEMA_URL_TOKEN") and request.tenant.schema_name == get_public_schema_name() and request.path_info[-1] == "/" ): # we are not at the public schema, manually alter routing to schema-dependent urls request.path_info = settings.PUBLIC_SCHEMA_URL_TOKEN + request.path_info if SET_TENANT_SITE_DYNAMICALLY and hasattr(request.tenant, "site") and request.tenant.site: SITE_THREAD_LOCAL.SITE_ID = request.tenant.site_id # dynamically set the site else: SITE_THREAD_LOCAL.SITE_ID = DEFAULT_SITE_ID
def sync_tenant_apps(self, schema_name=None): ContentType.objects.clear_cache() apps = self.tenant_apps or self.installed_apps self._set_managed_apps(apps) syncdb_command = SyncdbCommand() if schema_name: print self.style.NOTICE("=== Running syncdb for schema: %s" % schema_name) sync_tenant = get_tenant_model().objects.filter( schema_name=schema_name).get() connection.set_tenant(sync_tenant, include_public=False) syncdb_command.execute(**self.options) else: public_schema_name = get_public_schema_name() tenant_schemas_count = get_tenant_model().objects.exclude( schema_name=public_schema_name).count() if not tenant_schemas_count: print self.style.NOTICE("No tenants found") for tenant_schema in get_tenant_model().objects.exclude( schema_name=public_schema_name).all(): print self.style.NOTICE("=== Running syncdb for schema %s" % tenant_schema.schema_name) connection.set_tenant(tenant_schema, include_public=False) syncdb_command.execute(**self.options)
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: request.tenant = TenantModel.get_for_domain(hostname) connection.set_tenant(request.tenant) except TenantModel.DoesNotExist: raise self.TENANT_NOT_FOUND_EXCEPTION( 'No tenant for hostname "%s"' % hostname) # 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
def test_command(self): """ Tests that tenant_command is capable of wrapping commands and its parameters. """ settings.SHARED_APPS = ( "tenant_schemas", "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() tenant_command.Command().handle( "dumpdata", get_public_schema_name(), "tenant_schemas", natural_foreign=True, stdout=out, ) self.assertJSONEqual( out.getvalue(), [{ "fields": { "domain_url": "localhost", "schema_name": "public" }, "model": "tenant_schemas.tenant", "pk": 1, }], )
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) # 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
def test_command(self): """ Tests that tenant_command is capable of wrapping commands and its parameters. """ settings.SHARED_APPS = ( 'tenant_schemas', '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', 'tenant_schemas'), natural_foreign=True, schema_name=get_public_schema_name(), stdout=out) self.assertEqual( json.loads( '[{"fields": {"domain_url": "localhost", "schema_name": "public"}, ' '"model": "tenant_schemas.tenant", "pk": 1}]'), json.loads(out.getvalue()))
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 tenant_schemas.utils import get_public_schema_name, app_labels 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 if model_name: model = apps.get_model(app_label=app_label, model_name=model_name) if hasattr(model, '__schema_name__') and model.__schema_name__: conn_schema = connection.tenant.schema_name.strip() if conn_schema == 'public' and conn_schema != model.__schema_name__ or \ conn_schema != 'public' and model.__schema_name__ == 'public': return False return None
def migrate_tenant_apps(self, schema_name=None): self._save_south_settings() apps = self.tenant_apps or self.installed_apps self._set_managed_apps(included_apps=apps, excluded_apps=self.shared_apps) syncdb_command = MigrateCommand() if schema_name: print self.style.NOTICE("=== Running migrate for schema: %s" % schema_name) connection.set_schema_to_public() sync_tenant = get_tenant_model().objects.filter(schema_name=schema_name).get() connection.set_tenant(sync_tenant, include_public=False) syncdb_command.execute(**self.options) else: public_schema_name = get_public_schema_name() tenant_schemas_count = get_tenant_model().objects.exclude(schema_name=public_schema_name).count() if not tenant_schemas_count: print self.style.NOTICE("No tenants found") for tenant_schema in get_tenant_model().objects.exclude(schema_name=public_schema_name).all(): Migrations._dependencies_done = False # very important, the dependencies need to be purged from cache print self.style.NOTICE("=== Running migrate for schema %s" % tenant_schema.schema_name) connection.set_tenant(tenant_schema, include_public=False) syncdb_command.execute(**self.options) self._restore_south_settings()
def process_request(self, request): # Wei-Lin connection.set_schema_to_public() hostname_without_port = filter_url_path(request.path) if hostname_without_port in self.filter_first_path: print("public schema") else: TenantModel = get_tenant_model() try: request.tenant = TenantModel.objects.get( schema_name=hostname_without_port) except utils.DatabaseError: request.urlconf = settings.PUBLIC_SCHEMA_URLCONF return except TenantModel.DoesNotExist: if hostname_without_port in ("127.0.0.1", "localhost"): request.urlconf = settings.PUBLIC_SCHEMA_URLCONF return else: raise Http404 connection.set_tenant(request.tenant) ContentType.objects.clear_cache() if hasattr( settings, 'PUBLIC_SCHEMA_URLCONF' ) and request.tenant.schema_name == get_public_schema_name(): request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
def _hidden_component(self, component): if "tenant_schemas" not in settings.INSTALLED_APPS: # Only hide components when we are not in multi-tenant mode return False # For components only registered for specific schemas, return solely # based on that option. That is, you can't disable an explicitly # registered component for a given schema. schemas = self._schemas[component.__class__] if schemas: if connection.tenant.schema_name in schemas: return False return True application = component.__module__.split(".admin", 1)[0] logger.debug("application=%r", application) # Before we allow it through, make sure the application is available # on this tenant. if application in settings.INSTALLED_APPS: if connection.tenant.schema_name == get_public_schema_name(): if application not in settings.SHARED_APPS: return True else: if application not in settings.TENANT_APPS: return True # Lastly, check if the client allows the application in this tenant. try: return connection.tenant.hidden_component(component) except AttributeError: return False
def _cursor(self): """ Here it happens. We hope every Django db operation using PostgreSQL must go through this to get the cursor handle. We change the path. """ cursor = super(DatabaseWrapper, self)._cursor() # 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_identifier(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) cursor.execute('SET search_path = {}'.format(','.join(search_paths))) return cursor
def handle_noargs(self, **options): # todo: awful lot of duplication from sync_schemas sync_tenant = options.get('tenant') sync_public = options.get('shared') schema_name = options.get('schema_name') self.installed_apps = settings.INSTALLED_APPS self.options = options if sync_public and schema_name: raise CommandError( "schema should only be used with the --tenant switch.") if not sync_public and not sync_tenant and not schema_name: # no options set, sync both sync_tenant = True sync_public = True if schema_name: if schema_name == get_public_schema_name(): sync_public = True else: sync_tenant = True if hasattr(settings, 'TENANT_APPS'): self.tenant_apps = settings.TENANT_APPS if hasattr(settings, 'SHARED_APPS'): self.shared_apps = settings.SHARED_APPS if sync_public: self.migrate_public_apps() if sync_tenant: self.migrate_tenant_apps(schema_name)
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)
def setUpClass(cls): super(TenantDataAndSettingsTest, cls).setUpClass() settings.SHARED_APPS = ('tenant_schemas', ) settings.TENANT_APPS = ('dts_test_app', 'django.contrib.contenttypes', 'django.contrib.auth', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS cls.sync_shared() Tenant(domain_url='test.com', schema_name=get_public_schema_name()).save()
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)
def setUpClass(cls): super(RoutesTestCase, cls).setUpClass() settings.SHARED_APPS = ('tenant_schemas', ) 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())
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
def sync_tenant_apps(self, schema_name=None): if schema_name: tenant = get_tenant_model().objects.filter(schema_name=schema_name).get() self._sync_tenant(tenant) else: all_tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name()) if not all_tenants: self._notice("No tenants found!") for tenant in all_tenants: self._sync_tenant(tenant)
def set_tenant(self, tenant, include_public=True): """ Main API method to current database schema, but it does not actually modify the db connection. """ self.tenant = tenant if tenant is None: self.schema_name = get_public_schema_name() else: self.schema_name = tenant.schema_name self.include_public_schema = include_public
def handle(self, *args, **options): super(MigrateSchemasCommand, self).handle(*args, **options) self.PUBLIC_SCHEMA_NAME = get_public_schema_name() if self.sync_public and not self.schema_name: self.schema_name = self.PUBLIC_SCHEMA_NAME if self.sync_public: self.run_migrations(self.schema_name, settings.SHARED_APPS) if self.sync_tenant: if self.schema_name and self.schema_name != self.PUBLIC_SCHEMA_NAME: if not schema_exists(self.schema_name): raise RuntimeError('Schema "{}" does not exist'.format( self.schema_name)) else: self.run_migrations(self.schema_name, settings.TENANT_APPS) else: all_tenants = get_tenant_model().objects.exclude(schema_name=get_public_schema_name()) for tenant in all_tenants: self.run_migrations(tenant.schema_name, settings.TENANT_APPS)
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
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)
def save(self, verbosity=1, force_create=False, *args, **kwargs): is_new = self.pk is None or force_create if is_new and connection.schema_name not in (get_public_schema_name(), settings.DEFAULT_TENANT_SCHEMA): 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.is_active and self.auto_create_schema: try: self.create_schema(check_if_exists=True, verbosity=verbosity) post_schema_sync.send(sender=TenantMixin, tenant=self) except: # We failed creating the tenant, delete what we created and # re-raise the exception self.delete(force_drop=True) raise
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)
def restore_schema(task, **kwargs): """ Switches the schema back to the one from before running the task. """ from tenant_schemas.utils import get_public_schema_name schema_name, include_public = getattr(task, '_old_schema', (get_public_schema_name(), True)) # If the schema names match, don't do anything. if connection.schema_name == schema_name: return connection.set_schema(schema_name, include_public=include_public)
def delete(self, *args, **kwargs): """ Drops the schema related to the tenant instance. Just drop the schema if the parent class model has the attribute auto_drop_schema set to True. """ if connection.get_schema() 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.get_schema()) if schema_exists(self.schema_name) and self.auto_drop_schema: cursor = connection.cursor() cursor.execute('DROP SCHEMA %s CASCADE' % self.schema_name) super(TenantMixin, self).delete(*args, **kwargs)
def test_command(self): """ Tests that tenant_command is capable of wrapping commands and its parameters. """ settings.SHARED_APPS = ('tenant_schemas', '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() if django.VERSION >= (1, 8, 0): call_command('tenant_command', args=('dumpdata', 'tenant_schemas'), natural_foreign=True, schema_name=get_public_schema_name(), stdout=out) else: call_command('tenant_command', 'dumpdata', 'tenant_schemas', natural_foreign=True, schema_name=get_public_schema_name(), stdout=out) self.assertItemsEqual( json.loads('[{"fields": {"domain_url": "localhost", "schema_name": "public"}, ' '"model": "tenant_schemas.tenant", "pk": 1}]'), json.loads(out.getvalue()))
def allow_migrate(self, db, model): # 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 tenant_schemas.utils import get_public_schema_name, app_labels if connection.schema_name == get_public_schema_name(): if model._meta.app_label not in app_labels(settings.SHARED_APPS): return False else: if model._meta.app_label not in app_labels(settings.TENANT_APPS): return False return None
def switch_schema(task, kwargs, **kw): """ Switches schema of the task, before it has been run. """ # Lazily load needed functions, as they import django model functions which # in turn load modules that need settings to be loaded and we can't # guarantee this module was loaded when the settings were ready. from tenant_schemas.utils import get_public_schema_name, get_tenant_model old_schema = (connection.schema_name, connection.include_public_schema) setattr(task, '_old_schema', old_schema) # Pop it from the kwargs since tasks don't except the additional kwarg. # This change is transparent to the system. schema = kwargs.pop('_schema_name', get_public_schema_name()) # If the schema has not changed, don't do anything. if connection.schema_name == schema: return if connection.schema_name != get_public_schema_name(): connection.set_schema_to_public() tenant = get_tenant_model().objects.get(schema_name=schema) connection.set_tenant(tenant, include_public=True)