def setUpClass(cls): super(SharedAuthTest, cls).setUpClass() settings.SHARED_APPS = ('django_tenants', '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_urls=['test.com'], schema_name=get_public_schema_name()).save() # Create a tenant cls.tenant = Tenant(domain_urls=['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 ready(self): # Test for configuration recommendations. These are best practices, # they avoid hard to find bugs and unexpected behaviour. if not hasattr(settings, 'TENANT_APPS'): raise ImproperlyConfigured('TENANT_APPS setting not set') if not settings.TENANT_APPS: raise ImproperlyConfigured("TENANT_APPS is empty. " "Maybe you don't need this app?") if not hasattr(settings, 'TENANT_MODEL'): raise ImproperlyConfigured('TENANT_MODEL setting not set') if 'django_tenants.routers.TenantSyncRouter' not in settings.DATABASE_ROUTERS: raise ImproperlyConfigured( "DATABASE_ROUTERS setting must contain " "'django_tenants.routers.TenantSyncRouter'.") if hasattr(settings, 'PG_EXTRA_SEARCH_PATHS'): if get_public_schema_name() in settings.PG_EXTRA_SEARCH_PATHS: raise ImproperlyConfigured( "%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: raise ImproperlyConfigured( "Do not include tenant schemas (%s) on PG_EXTRA_SEARCH_PATHS." % list(invalid_schemas))
def ready(self): # Test for configuration recommendations. These are best practices, # they avoid hard to find bugs and unexpected behaviour. if not hasattr(settings, "TENANT_APPS"): raise ImproperlyConfigured("TENANT_APPS setting not set") if not settings.TENANT_APPS: raise ImproperlyConfigured("TENANT_APPS is empty. " "Maybe you don't need this app?") if not hasattr(settings, "TENANT_MODEL"): raise ImproperlyConfigured("TENANT_MODEL setting not set") if "django_tenants.routers.TenantSyncRouter" not in settings.DATABASE_ROUTERS: raise ImproperlyConfigured( "DATABASE_ROUTERS setting must contain " "'django_tenants.routers.TenantSyncRouter'." ) if hasattr(settings, "PG_EXTRA_SEARCH_PATHS"): if get_public_schema_name() in settings.PG_EXTRA_SEARCH_PATHS: raise ImproperlyConfigured( "%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: raise ImproperlyConfigured( "Do not include tenant schemas (%s) on PG_EXTRA_SEARCH_PATHS." % list(invalid_schemas) )
def setUpClass(cls): super(SharedAuthTest, cls).setUpClass() settings.SHARED_APPS = ( 'django_tenants', '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_urls=['test.com'], schema_name=get_public_schema_name()).save() # Create a tenant cls.tenant = Tenant(domain_urls=['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 test_signal_on_migrate_schemas(self): """ Check signals are sent on running of migrate_schemas. """ executor = get_executor() tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() # test the signal gets called when running migrate with catch_signal(schema_migrated) as handler: call_command('migrate_schemas', interactive=False, verbosity=0) if executor == 'simple': handler.assert_has_calls([ mock.call( schema_name=get_public_schema_name(), sender=mock.ANY, signal=schema_migrated, ), mock.call(schema_name='test', sender=mock.ANY, signal=schema_migrated) ]) elif executor == 'multiprocessing': # public schema gets migrated in the current process, always handler.assert_called_once_with( schema_name=get_public_schema_name(), sender=mock.ANY, signal=schema_migrated, )
def delete_tenant(self): """ We don't actually delete the tenant out of the database, but we associate them with a the public schema user and change their url to reflect their delete datetime and previous owner. The caller should verify that the user deleting the tenant owns the tenant. """ # Prevent public tenant schema from being deleted if self.schema_name == get_public_schema_name(): raise ValueError("Cannot delete public tenant schema") for user_obj in self.users.all(): self.remove_user(user_obj) # Seconds since epoch, time() returns a float, so we convert to # an int first to truncate the decimal portion time_string = str(int(time.time())) new_url = "{}-{}-{}".format(time_string, str(self.owner.id), self.domain_url) self.domain_url = new_url # The schema generated each time (even with same url slug) will be unique. # So we do not have to worry about a conflict with that # Set the owner to the system user (public schema owner) public_tenant = get_tenant_model().objects.get( schema_name=get_public_schema_name()) # Transfer ownership to system self.transfer_ownership(public_tenant.owner)
def setUp(self): super().setUp() settings.SHARED_APPS = ('django_tenants', 'customers', 'django.contrib.auth', 'django.contrib.contenttypes', ) settings.TENANT_APPS = ('dts_test_app', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS self.sync_shared() self.public_tenant = get_tenant_model()(schema_name=get_public_schema_name()) self.public_tenant.save() self.public_domain = get_tenant_domain_model()(tenant=self.public_tenant, domain='test.com') self.public_domain.save() # Create a tenant self.tenant = get_tenant_model()(schema_name='tenant') self.tenant.save() self.domain = get_tenant_domain_model()(tenant=self.tenant, domain='tenant.test.com') self.domain.save() # Create some users with schema_context(get_public_schema_name()): # this could actually also be executed inside a tenant self.user1 = User(username='******', email="*****@*****.**") self.user1.save() self.user2 = User(username='******', email="*****@*****.**") self.user2.save() # Create instances on the tenant that point to the users on public with tenant_context(self.tenant): self.d1 = ModelWithFkToPublicUser(user=self.user1) self.d1.save() self.d2 = ModelWithFkToPublicUser(user=self.user2) self.d2.save()
def setUp(self): super(SharedAuthTest, self).setUp() settings.SHARED_APPS = ('django_tenants', 'customers', 'django.contrib.auth', 'django.contrib.contenttypes', ) settings.TENANT_APPS = ('dts_test_app', ) settings.INSTALLED_APPS = settings.SHARED_APPS + settings.TENANT_APPS self.sync_shared() self.public_tenant = get_tenant_model()(schema_name=get_public_schema_name()) self.public_tenant.save() self.public_domain = get_tenant_domain_model()(tenant=self.public_tenant, domain='test.com') self.public_domain.save() # Create a tenant self.tenant = get_tenant_model()(schema_name='tenant') self.tenant.save() self.domain = get_tenant_domain_model()(tenant=self.tenant, domain='tenant.test.com') self.domain.save() # Create some users with schema_context(get_public_schema_name()): # this could actually also be executed inside a tenant self.user1 = User(username='******', email="*****@*****.**") self.user1.save() self.user2 = User(username='******', email="*****@*****.**") self.user2.save() # Create instances on the tenant that point to the users on public with tenant_context(self.tenant): self.d1 = ModelWithFkToPublicUser(user=self.user1) self.d1.save() self.d2 = ModelWithFkToPublicUser(user=self.user2) self.d2.save()
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
def setUp(self): super().setUp() # 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() with connection.cursor() as cursor: cursor.execute('DROP SCHEMA %s CASCADE; CREATE SCHEMA %s;' % (get_public_schema_name(), get_public_schema_name(), )) self.sync_shared()
def setUp(self): super(BaseSyncTest, self).setUp() # 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() with connection.cursor() as cursor: cursor.execute('DROP SCHEMA %s CASCADE; CREATE SCHEMA %s;' % (get_public_schema_name(), get_public_schema_name(), )) self.sync_shared()
def process_request(self, request): # Short circuit if tenant is already set by another middleware. # This allows for multiple tenant-resolving middleware chained together. if hasattr(request, "tenant"): return connection.set_schema_to_public() tenant = None urlconf = None TenantModel = get_tenant_model() hostname = self.hostname_from_request(request) subfolder_prefix_path = "/{}/".format(get_subfolder_prefix()) # We are in the public tenant if not request.path.startswith(subfolder_prefix_path): try: tenant = TenantModel.objects.get( schema_name=get_public_schema_name()) except TenantModel.DoesNotExist: raise self.TENANT_NOT_FOUND_EXCEPTION( "Unable to find public tenant") # Do we have a public-specific urlconf? if (hasattr(settings, "PUBLIC_SCHEMA_URLCONF") and tenant.schema_name == get_public_schema_name()): urlconf = settings.PUBLIC_SCHEMA_URLCONF # We are in a specific tenant else: path_chunks = request.path[len(subfolder_prefix_path):].split("/") tenant_subfolder = path_chunks[0] try: tenant = TenantModel.objects.get( domains__domain=tenant_subfolder) except TenantModel.DoesNotExist: raise self.TENANT_NOT_FOUND_EXCEPTION( 'No tenant for subfolder "%s"' % (tenant_subfolder or "")) tenant.domain_subfolder = tenant_subfolder urlconf = get_subfolder_urlconf(tenant) tenant.domain_url = hostname request.tenant = tenant connection.set_tenant(request.tenant) clear_url_caches( ) # Required to remove previous tenant prefix from cache, if present if urlconf: request.urlconf = urlconf set_urlconf(urlconf)
def ready(self): from django.db import connections # Test for configuration recommendations. These are best practices, # they avoid hard to find bugs and unexpected behaviour. if not hasattr(settings, 'TENANT_APPS'): raise ImproperlyConfigured('TENANT_APPS setting not set') if not settings.TENANT_APPS: raise ImproperlyConfigured("TENANT_APPS is empty. " "Maybe you don't need this app?") if not hasattr(settings, 'TENANT_MODEL'): raise ImproperlyConfigured('TENANT_MODEL setting not set') if not hasattr(settings, 'TENANT_SESSION_KEY'): setattr(settings, 'TENANT_SESSION_KEY', 'tenant_schema') if not hasattr(settings, 'TENANT_SELECTION_METHOD'): setattr(settings, 'TENANT_SELECTION_METHOD', 'domain') if not hasattr(settings, 'TENANT_DATABASE'): setattr(settings, 'TENANT_DATABASE', 'default') if 'django_tenants.routers.TenantSyncRouter' not in settings.DATABASE_ROUTERS: raise ImproperlyConfigured( "DATABASE_ROUTERS setting must contain " "'django_tenants.routers.TenantSyncRouter'.") if hasattr(settings, 'PG_EXTRA_SEARCH_PATHS'): if get_public_schema_name() in settings.PG_EXTRA_SEARCH_PATHS: raise ImproperlyConfigured( "%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 # first check that the model table is created model = get_tenant_model() c = connections[DEFAULT_DB_ALIAS].cursor() c.execute( 'SELECT 1 FROM information_schema.tables WHERE table_name = %s;', [model._meta.db_table]) if c.fetchone(): invalid_schemas = set( settings.PG_EXTRA_SEARCH_PATHS).intersection( model.objects.all().values_list('schema_name', flat=True)) if invalid_schemas: raise ImproperlyConfigured( "Do not include tenant schemas (%s) on PG_EXTRA_SEARCH_PATHS." % list(invalid_schemas))
def setUpClass(cls): settings.TENANT_MODEL = "django_tenants.Tenant" settings.SHARED_APPS = ("django_tenants",) 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 ready(self): from django.db import connections # Test for configuration recommendations. These are best practices, # they avoid hard to find bugs and unexpected behaviour. if not hasattr(settings, 'TENANT_APPS'): raise ImproperlyConfigured('TENANT_APPS setting not set') if not settings.TENANT_APPS: raise ImproperlyConfigured("TENANT_APPS is empty. " "Maybe you don't need this app?") if not hasattr(settings, 'TENANT_MODEL'): raise ImproperlyConfigured('TENANT_MODEL setting not set') if not hasattr(settings, 'TENANT_SESSION_KEY'): setattr(settings, 'TENANT_SESSION_KEY', 'tenant_schema') if not hasattr(settings, 'TENANT_SELECTION_METHOD'): setattr(settings, 'TENANT_SELECTION_METHOD', 'domain') if not hasattr(settings, 'TENANT_DATABASE'): setattr(settings, 'TENANT_DATABASE', 'default') if 'django_tenants.routers.TenantSyncRouter' not in settings.DATABASE_ROUTERS: raise ImproperlyConfigured("DATABASE_ROUTERS setting must contain " "'django_tenants.routers.TenantSyncRouter'.") if hasattr(settings, 'PG_EXTRA_SEARCH_PATHS'): if get_public_schema_name() in settings.PG_EXTRA_SEARCH_PATHS: raise ImproperlyConfigured( "%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 # first check that the model table is created model = get_tenant_model() c = connections[DEFAULT_DB_ALIAS].cursor() c.execute( 'SELECT 1 FROM information_schema.tables WHERE table_name = %s;', [model._meta.db_table] ) if c.fetchone(): invalid_schemas = set(settings.PG_EXTRA_SEARCH_PATHS).intersection( model.objects.all().values_list('schema_name', flat=True)) if invalid_schemas: raise ImproperlyConfigured( "Do not include tenant schemas (%s) on PG_EXTRA_SEARCH_PATHS." % list(invalid_schemas))
def get(self, request, name=None): results = [] if name: if name == 'SuperPOEM_Tenant': tenants = Tenant.objects.filter( schema_name=get_public_schema_name()) else: tenants = Tenant.objects.filter(name=name) if len(tenants) == 0: return Response({'detail': 'Tenant not found.'}, status=status.HTTP_404_NOT_FOUND) else: tenants = Tenant.objects.all() for tenant in tenants: if tenant.schema_name == get_public_schema_name(): tenant_name = 'SuperPOEM Tenant' metric_key = 'metric_templates' else: tenant_name = tenant.name metric_key = 'metrics' try: domain = get_tenant_domain_model().objects.get(tenant=tenant) data = get_tenant_resources(tenant.schema_name) results.append( dict(name=tenant_name, schema_name=tenant.schema_name, domain_url=domain.domain, created_on=datetime.date.strftime( tenant.created_on, '%Y-%m-%d'), nr_metrics=data[metric_key], nr_probes=data['probes'])) except get_tenant_domain_model().DoesNotExist: return error_response( status_code=status.HTTP_404_NOT_FOUND, detail='Domain for tenant {} not found.'.format( tenant_name)) if name: results = results[0] else: results = sorted(results, key=lambda k: k['name'].lower()) return Response(results)
def ready(self): from django.db import connection # Test for configuration recommendations. These are best practices, # they avoid hard to find bugs and unexpected behaviour. if hasattr(settings, 'HAS_MULTI_TYPE_TENANTS') and settings.HAS_MULTI_TYPE_TENANTS: if not hasattr(settings, 'TENANT_TYPES'): raise ImproperlyConfigured('Using multi type you must setup TENANT_TYPES setting') if get_public_schema_name() not in settings.TENANT_TYPES: raise ImproperlyConfigured('get_public_schema_name() value not found as a key in TENANTS') if not hasattr(settings, 'MULTI_TYPE_DATABASE_FIELD'): raise ImproperlyConfigured('Using multi type you must setup MULTI_TYPE_DATABASE_FIELD setting') else: if not hasattr(settings, 'TENANT_APPS'): raise ImproperlyConfigured('TENANT_APPS setting not set') if not settings.TENANT_APPS: raise ImproperlyConfigured("TENANT_APPS is empty. " "Maybe you don't need this app?") if not hasattr(settings, 'TENANT_MODEL'): raise ImproperlyConfigured('TENANT_MODEL setting not set') tenant_sync_router = getattr(settings, 'TENANT_SYNC_ROUTER', 'django_tenants.routers.TenantSyncRouter') if tenant_sync_router not in settings.DATABASE_ROUTERS: raise ImproperlyConfigured("DATABASE_ROUTERS setting must contain " "'%s'." % tenant_sync_router) validate_extra_extensions()
def connect_user(self, user_obj, tenant_user): """ Connect a public user to a tenant user. """ if self.schema_name == get_public_schema_name(): raise SchemaError( "It's not allowed to connect a public user to a public user." "Make sure the current tenant {} is not the " "public tenant.".format(self)) self._check_user_exists(user_obj) # The tenant user already linked to a public user if tenant_user.supervisor is not None: raise ExistsError( "The tenant user already linked to public user: {}".format( tenant_user.supervisor)) tenant_user.supervisor = user_obj tenant_user.save(update_fields=['supervisor']) # Link user to tenant fields = self.__class__.users.field.remote_field.through_fields + ( 'organization_user', ) self.__class__.users.through._default_manager.create( **dict(zip(fields, (self, user_obj, tenant_user.pk)))) tenant_user_connected.send(sender=self.__class__, user=user_obj, tenant=self, tenant_user=tenant_user)
def is_shared_app(app): if has_multi_type_tenants(): _apps = get_tenant_types()[get_public_schema_name()]['APPS'] else: _apps = settings.SHARED_APPS return app["app_label"] in [get_app_label(_app) for _app in _apps]
def create_public_tenant(domain_url, owner_username, owner_email, **owner_extra): UserModel = get_user_model() TenantModel = get_tenant_model() public_schema_name = get_public_schema_name() if TenantModel.objects.filter(schema_name=public_schema_name).first(): raise ExistsError("Public tenant already exists.") # Create public tenant user. This user doesn't go through object manager # create_user function because public tenant does not exist yet profile = UserModel.objects.create(username=owner_username, email=owner_email, is_active=True, **owner_extra) profile.set_unusable_password() profile.save() # Create public tenant public_tenant = TenantModel.objects.create(schema_name=public_schema_name, name='Public Tenant', owner=profile) # Add one or more domains for the tenant get_tenant_domain_model().objects.create(domain=domain_url, tenant=public_tenant, is_primary=True)
def delete(self, request, name=None): if name: if request.tenant.schema_name == get_public_schema_name(): if request.user.is_superuser: try: tenant = Tenant.objects.get(name=name) tenant.delete() return Response(status=status.HTTP_204_NO_CONTENT) except Tenant.DoesNotExist: return error_response( status_code=status.HTTP_404_NOT_FOUND, detail='Tenant not found.') else: return error_response( status_code=status.HTTP_401_UNAUTHORIZED, detail='You do not have permission to delete tenants.') else: return error_response( status_code=status.HTTP_403_FORBIDDEN, detail='Cannot delete tenant outside public schema.') else: return error_response(status_code=status.HTTP_400_BAD_REQUEST, detail='Tenant name should be specified.')
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 = ('django_tenants', # 2 tables 'customers', '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 = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() 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 execute_sync(country_pk, synchronizer): country = Country.objects.get(pk=country_pk) if country.schema_name == get_public_schema_name(): vision_sync_task(synchronizers=[synchronizer, ]) else: sync_handler.delay(country.name, synchronizer) return HttpResponseRedirect(reverse('admin:users_country_change', args=[country.pk]))
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 = ( 'django_tenants', # 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_urls=['arbitrary.test.com'], schema_name='test') tenant.save() 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 delete(self, request, name): if request.tenant.schema_name == get_public_schema_name() and \ request.user.is_superuser: try: mts = admin_models.MetricTemplate.objects.filter( tags__name=name) tag = admin_models.MetricTags.objects.get(name=name) for mt in mts: mt.tags.remove(tag) update_metrics(mt, mt.name, mt.probekey) tag.delete() return Response(status=status.HTTP_204_NO_CONTENT) except admin_models.MetricTags.DoesNotExist: return error_response( status_code=status.HTTP_404_NOT_FOUND, detail="The requested metric tag does not exist.") else: return error_response( status_code=status.HTTP_401_UNAUTHORIZED, detail="You do not have permission to delete metric tags.")
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() tenant_model = get_tenant_model() user = getattr(request, 'user', None) try: tenant = self.get_tenant(tenant_model, user) except tenant_model.DoesNotExist: tenant = tenant_model.objects.get(schema_name=get_public_schema_name()) 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
def delete(self, request, name=None, tag=None): if request.tenant.schema_name == get_public_schema_name() and \ request.user.is_superuser: if name and tag: if tag == 'centos6': ostag = admin_models.OSTag.objects.get(name='CentOS 6') elif tag == 'centos7': ostag = admin_models.OSTag.objects.get(name='CentOS 7') else: raise NotFound(status=404, detail='OS tag does not exist.') try: admin_models.YumRepo.objects.get(name=name, tag=ostag).delete() return Response(status=status.HTTP_204_NO_CONTENT) except admin_models.YumRepo.DoesNotExist: raise NotFound(status=404, detail='YUM repo does not exist.') else: return error_response( status_code=status.HTTP_400_BAD_REQUEST, detail='YUM repo name and/or tag should be specified.') else: return error_response( status_code=status.HTTP_401_UNAUTHORIZED, detail='You do not have permission to delete YUM repos.')
def post(self, request): if request.tenant.schema_name == get_public_schema_name() and \ request.user.is_superuser: try: admin_models.YumRepo.objects.create( name=request.data['name'], tag=admin_models.OSTag.objects.get( name=request.data['tag']), content=request.data['content'], description=request.data['description']) return Response(status=status.HTTP_201_CREATED) except IntegrityError: return error_response( status_code=status.HTTP_400_BAD_REQUEST, detail='YUM repo with this name and tag already exists.') except admin_models.OSTag.DoesNotExist: return error_response(status_code=status.HTTP_404_NOT_FOUND, detail='OS tag does not exist.') except KeyError as e: return error_response(status_code=status.HTTP_400_BAD_REQUEST, detail='Missing data key: {}'.format( e.args[0])) else: return error_response( status_code=status.HTTP_401_UNAUTHORIZED, detail='You do not have permission to add YUM repos.')
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 executor = get_executor(codename=self.executor)(self.args, self.options) if self.sync_public: executor.run_migrations(tenants=[self.PUBLIC_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 RuntimeError('Schema "{}" does not exist'.format( self.schema_name)) else: tenants = [self.schema_name] else: tenants = get_tenant_model().objects.only( 'schema_name').exclude( schema_name=self.PUBLIC_SCHEMA_NAME).values_list( 'schema_name', flat=True) executor.run_migrations(tenants=tenants)
def add_user(self, user_obj, is_superuser=False, is_staff=False): """ Create a user inside the tenant and set its supervisor to the public user. """ if self.schema_name == get_public_schema_name(): raise SchemaError( "It's not allowed to add a public user to the public tenant." "Make sure the current tenant {} is not the " "public tenant.".format(self)) self._check_user_exists(user_obj) # Create a user in the tenant with generated username and email # And link it to the public user time_string = str(int(time.time())) tenant_user = get_tenant_user_model().objects.create( email='{}_{}'.format(user_obj.email, time_string), username='******'.format(user_obj.username, time_string), supervisor=user_obj, is_superuser=is_superuser, is_staff=is_staff, is_verified=True) # Link user to tenant self._link_to_tenant_user(user_obj, tenant_user) tenant_user_added.send(sender=self.__class__, user=user_obj, tenant=self)
def is_shared_app(app): if has_multi_type_tenants(): _apps = get_tenant_types()[get_public_schema_name()]['APPS'] else: _apps = settings.SHARED_APPS return app_in_list(app['app_label'], _apps)
def test_PG_EXTRA_SEARCH_PATHS(self): del apps.all_models['django_tenants'] c = connection.cursor() c.execute('DROP SCHEMA {0} CASCADE; CREATE SCHEMA {0};'.format( get_public_schema_name() )) apps.set_installed_apps(['customers', 'django_tenants'])
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 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 connections from django_tenants.utils import get_public_schema_name, get_tenant_database_alias if db != get_tenant_database_alias(): return False connection = connections[db] public_schema_name = get_public_schema_name() if has_multi_type_tenants(): tenant_types = get_tenant_types() if connection.schema_name == public_schema_name: installed_apps = tenant_types[public_schema_name]['APPS'] else: installed_apps = tenant_types[ connection.tenant.tenant_type]['APPS'] else: if connection.schema_name == public_schema_name: installed_apps = settings.SHARED_APPS else: installed_apps = settings.TENANT_APPS if not self.app_in_list(app_label, installed_apps): return False return None
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 executor = get_executor(codename=self.executor)(self.args, self.options) if self.sync_public: executor.run_migrations(tenants=[self.PUBLIC_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 RuntimeError('Schema "{}" does not exist'.format( self.schema_name)) else: tenants = [self.schema_name] else: tenants = get_tenant_model().objects.only( 'schema_name').exclude( schema_name=self.PUBLIC_SCHEMA_NAME).values_list( 'schema_name', flat=True) executor.run_migrations(tenants=tenants) tenant_objects = get_tenant_model().objects.filter(schema_name__in=tenants) for tenant in tenant_objects: if not isinstance(tenant, TenantMixin): continue post_schema_migrate.send(sender=TenantMixin, tenant=tenant.serializable_fields())
def get_user(request): if not hasattr(request, '_cached_user'): if request.tenant.schema_name == get_public_schema_name(): request._cached_user = auth.get_user(request) else: request._cached_user = get_tenant_user(request) return request._cached_user
def process_request(self, request): connection.set_schema_to_public() hostname_without_port = remove_www_and_dev( request.get_host().split(':')[0]) try: domain = get_tenant_domain_model().objects.select_related( 'tenant').get(domain=hostname_without_port) request.tenant = domain.tenant except utils.DatabaseError: request.urlconf = settings.PUBLIC_SCHEMA_URLCONF return except get_tenant_domain_model().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 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 update_metric_history(sender, instance, created, **kwargs): if not created: schemas = list( Tenant.objects.all().values_list('schema_name', flat=True) ) schemas.remove(get_public_schema_name()) probes = admin_models.ProbeHistory.objects.filter( package=instance ) for schema in schemas: with schema_context(schema): for probe in probes: metrics = Metric.objects.filter(probekey=probe) for metric in metrics: vers = TenantHistory.objects.filter( object_id=metric.id ).order_by('-date_created') for ver in vers: serialized_data = json.loads(ver.serialized_data) serialized_data[0]['fields']['probekey'] = [ probe.name, probe.package.version ] ver.serialized_data = json.dumps(serialized_data) ver.save()
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. """ 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)
def setUpClass(cls): super(TenantDataAndSettingsTest, cls).setUpClass() settings.SHARED_APPS = ('django_tenants', ) 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_urls=['test.com'], schema_name=get_public_schema_name()).save()
def get_tenant(self, domain_model, hostname): try: return super().get_tenant(domain_model, hostname) except domain_model.DoesNotExist: schema_name = self.DEFAULT_SCHEMA_NAME if not schema_name: schema_name = get_public_schema_name() tenant_model = get_tenant_model() return tenant_model.objects.get(schema_name=schema_name)
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 or self._previous_cursor != cursor: # Store the cursor pointer to check if it has changed since we # last validated. self._previous_cursor = 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_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 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, options) 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, options) 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, options)
def restore_schema(task, **kwargs): """ Switches the schema back to the one from before running the task. """ from django_tenants.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 setUpClass(cls): super(RoutesTestCase, cls).setUpClass() settings.SHARED_APPS = ('django_tenants', 'customers') 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 = get_tenant_model()(schema_name=get_public_schema_name()) cls.public_tenant.save() cls.public_domain = get_tenant_domain_model()(domain='test.com', tenant=cls.public_tenant) cls.public_domain.save()
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 verify_need_change_tenant_dir(self): if connection.schema_name != get_public_schema_name() and \ self.current_tenant_dir_name != self.get_tenant_dir_name(): print "TenantFileSystemFinder change config $$$$$$$$$$$$$$$$$$$$ ===============" print "self.current_tenant_dir_name: {0}".format(self.current_tenant_dir_name) print "tenant_dir_name: {0}".format(self.get_tenant_dir_name()) # print "TenantSiteFileSystemFinder change config $$$$$$$$$$$$$$$$$$$$ ===============" # print "self.current_tenant_dir_name: {}".format(self.current_tenant_dir_name) # print "self.site_dir: {}".format(self.site_dir) # print "connection.schema_name: {}".format(connection.schema_name) # print "bucket_static_name: {}".format(self.get_bucket_static_name()) self.set_tenant_dir_name() self.config_by_tenant_dir()
def test_restoring_schema_name(self): update_task.apply_async( args=(self.dummy1.pk, 'updated-name'), kwargs={'_schema_name': self.tenant1.schema_name} ) self.assertEqual(connection.schema_name, get_public_schema_name()) connection.set_tenant(self.tenant1) update_task.apply_async( args=(self.dummy2.pk, 'updated-name'), kwargs={'_schema_name': self.tenant2.schema_name} ) self.assertEqual(connection.schema_name, self.tenant1.schema_name) connection.set_tenant(self.tenant2) # The model does not exist in the public schema. with self.assertRaises(DummyModel.DoesNotExist): update_task.apply_async( args=(self.dummy2.pk, 'updated-name'), kwargs={'_schema_name': get_public_schema_name()} ) self.assertEqual(connection.schema_name, self.tenant2.schema_name)
def setUpClass(cls): super(TenantDataAndSettingsTest, cls).setUpClass() settings.SHARED_APPS = ('django_tenants', 'customers') 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 = get_tenant_model()(schema_name=get_public_schema_name()) tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='test.com') domain.save()
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 django_tenants.utils import get_public_schema_name if connection.schema_name == get_public_schema_name(): if not self.app_in_list(app_label, settings.SHARED_APPS): return False else: if not self.app_in_list(app_label, 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 django_tenants.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)
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 %s CASCADE' % self.schema_name) super(TenantMixin, self).delete(*args, **kwargs)
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) 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 elif is_new: schema_needs_to_be_sync.send(sender=TenantMixin, tenant=self)
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 = ('django_tenants', # 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)
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. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='something.test.com') domain.save() 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)
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. """ tenant = get_tenant_model()(schema_name='test') tenant.save() domain = get_tenant_domain_model()(tenant=tenant, domain='arbitrary.test.com') domain.save() 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 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 django_tenants.utils import get_public_schema_name # for INSTALLED_APPS we need a name from django.apps import apps app_name = apps.get_app_config(app_label).name if connection.schema_name == get_public_schema_name(): if app_name not in settings.SHARED_APPS: return False else: if app_name not in settings.TENANT_APPS: return False return None