I have a problem with django-tenants. I am still learning to program, so it is possible that I have made a beginner’s mistake somewhere.
I will explain what I am trying to achieve with a model example. Some procedures are quite awkward and serve mainly to identify the problem.
The problem is that the middleware likely does not switch the tenant. Specifically, I would expect that if /prefix/domain_idorsubfolder_id is in the URL, the middleware would automatically detect the prefix, subfolder ID, and set the corresponding schema as active. However, this is not happening, and the login to the tenant does not occur. Instead, the login happens in the "public" schema in the database.
Model example:
A user goes to http://127.0.0.1:8000/login/ and enters their email, which filters the appropriate tenant and redirects the user to /client/tenant_id/tenant/login.
Page not found (404)
Request Method: GET
Request URL: http://127.0.0.1:8000/client/test2f0d3775/tenant/login/
Using the URLconf defined in seo_app.tenant_urls_dynamically_tenant_prefixed, Django tried these URL patterns, in this order:
client/test2f0d3775/ basic-keyword-cleaning/ [name='basic_cleaned_keyword']
client/test2f0d3775/ ai-keyword-cleaning/ [name='auto_cleaned_keyword']
client/test2f0d3775/ keyword-group-creation/ [name='content_group']
client/test2f0d3775/ looking-for-link-oppurtunities/ [name='search_linkbuilding']
client/test2f0d3775/ url-pairing/ [name='url_pairing']
client/test2f0d3775/ creating-an-outline/ [name='article_outline']
client/test2f0d3775/ analyses/ [name='all_analyses']
client/test2f0d3775/ download/<str:model_type>/<int:file_id>/ [name='download_file']
client/test2f0d3775/ dashboard/ [name='dashboard']
client/test2f0d3775/ client/
The current path, client/test2f0d3775/tenant/login/, didn’t match any of these.
Notice that the authorization_app is not listed among the installed apps at all, which is strange because I reference it in urls.py, tenant_urls.py, and in the authorization_app’s own urls.py.
urls.py – public URLs in root_django_project
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("authorization_app.urls")),
path('', include('seo_app.tenant_urls')),
# path("client/", include('seo_app.tenant_urls_dynamically_tenant_prefixed')),
# path(r"", include("keyword_analysis_app.urls")),
# path(r"", include("linkbuilding_app.urls")),
# path(r"", include("content_app.urls")),
# path(r"", include("downloading_reports_app.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
tenant_urls.py – root_django_project
from django.urls import path, include
from django.conf import settings
app_name = "tenant"
urlpatterns = [
path(f"", include("keyword_analysis_app.urls")),
path(f"", include("linkbuilding_app.urls")),
path(f"", include("content_app.urls")),
path(f'', include("downloading_reports_app.urls")),
path(f'{settings.TENANT_SUBFOLDER_PREFIX}/', include("authorization_app.urls")),
# path("", include("keyword_analysis_app.urls")),
# path("", include("linkbuilding_app.urls")),
# path("", include("content_app.urls")),
# path('', include("downloading_reports_app.urls")),
# path('', include("authorization_app.urls")),
]
urls.py – authorization_app
from django.urls import path
from . import views
from django.contrib.auth.views import LogoutView
from .views import CustomLoginView, redirect_tenant_login
app_name = "authorization_app"
urlpatterns = [
path("register/", views.user_register, name="register"),
path("login/", views.redirect_tenant_login, name="redirect_tenant"),
path("logout/", LogoutView.as_view(next_page="/"), name="logout"),
path("<str:subfolder>/tenant/login/", CustomLoginView.as_view(), name="login_tenant"),
]
views.py – authorization_app
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib.auth.views import LoginView
from .forms import UserRegisterForm, RedirectTenantForm
from tenants_app.models import TenantModel, DomainModel
from django.db import transaction
from django.contrib.auth.decorators import login_required
from .models import UserModel
import uuid
import urllib.parse
import re
from seo_app import settings
import logging
from django_tenants.utils import schema_context
from django.contrib.auth.models import Group, Permission
from tenant_users.permissions.models import UserTenantPermissions
from django.db import connection
@transaction.atomic
def user_register(request):
if request.method == "POST":
form = UserRegisterForm(request.POST)
if form.is_valid():
new_user = form.save(commit=False) # Save new user to the database. Then add them as a foreign key to owner_id
new_user.is_active = True
new_user.save()
subdomain_username = form.cleaned_data.get("username").lower()
subdomain_username = re.sub(r'[^a-zA-Z0-9]', '', subdomain_username) # This regex cleans the user input for use in subdomain
unique_id = str(uuid.uuid4()).replace('-', '').lower()[:7]
subdomain_username = f"{subdomain_username}{unique_id}"
# Create tenant instance
new_user_tenant = TenantModel()
new_user_tenant.owner_id = new_user.id
new_user_tenant.name = subdomain_username
new_user_tenant.schema_name = subdomain_username
new_user_tenant.domain_subfolder = subdomain_username
new_user_tenant.save()
# Set domain instance for User
new_user_subdomain = DomainModel()
new_user_subdomain.domain = f"{subdomain_username}"
new_user_subdomain.tenant = new_user_tenant
new_user_subdomain.is_primary = True
new_user_subdomain.save()
return redirect("/")
else:
form = UserRegisterForm()
return render(request, "register_template.html", {"form": form})
def redirect_tenant_login(request):
if request.method == "POST":
form = RedirectTenantForm(request.POST)
if form.is_valid():
email = form.cleaned_data["tenant_email"]
user_instance = UserModel.objects.get(email=email)
tenant_instance = TenantModel.objects.get(owner_id=user_instance.id)
subdomain = tenant_instance.schema_name
print(f"User ID: {subdomain}")
return redirect(f"/client/{subdomain}/tenant/login/")
else:
form = RedirectTenantForm()
return render(request, "registration/redirect_tenant.html", {"form": form})
class CustomLoginView(LoginView): # Encode using urllib.parse
def get_success_url(self):
user = self.request.user
print(user)
try:
usermodel = UserModel.objects.get(email=user.email)
print(usermodel)
tenant_model = TenantModel.objects.get(owner_id=usermodel.id)
print(tenant_model)
tenant_name = tenant_model.schema_name
print(tenant_name)
tenant_name_normalized = urllib.parse.quote(tenant_name.lower()) # Safe encoding for URL
print(tenant_name_normalized)
return f'/client/{tenant_name_normalized}/dashboard/'
except UserModel.DoesNotExist:
print("Error 1")
return "/"
except AttributeError:
print("Error 2")
# Added to catch errors when 'tenant' does not exist for the given user
return "/"
def form_valid(self, form):
super().form_valid(form) # Standard login and get user
url = self.get_success_url()
return redirect(url)
At this URL http://127.0.0.1:8000/client/test2f0d3775/tenant/login/, I activated the Python manage.py shell and tested the connection, which returned the following:
>>> from django.db import connection
>>> print(connection.schema_name)
public
django – settings.py
from django.core.management.utils import get_random_secret_key
from pathlib import Path
import os
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = True
ALLOWED_HOSTS = ['localhost', '.localhost', '127.0.0.1', '.127.0.0.1', '*']
# Tenant Application definition
SHARED_APPS = (
'django_tenants',
'django.contrib.contenttypes',
"tenant_users.permissions",
"tenant_users.tenants",
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.admin',
'django.contrib.staticfiles',
'django.contrib.auth',
'authorization_app',
'tenants_app',
# everything below here is optional
)
TENANT_APPS = (
'django.contrib.auth',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.contenttypes',
"tenant_users.permissions",
'keyword_analysis_app',
'linkbuilding_app',
'content_app',
'downloading_reports_app',
'authorization_app',
)
TENANT_MODEL = "tenants_app.TenantModel" # app.Model
TENANT_DOMAIN_MODEL = "tenants_app.DomainModel" # app.Model
INSTALLED_APPS = list(SHARED_APPS) + [app for app in TENANT_APPS if app not in SHARED_APPS]
DEFAULT_SCHEMA_NAME = "public"
PUBLIC_SCHEMA_URLCONF = 'seo_app.urls' # points to public tenants in seo_app urls.py
# TENANT_URLCONF = 'seo_app.tenant_urls' # points to private tenants in seo_app urls.py
ROOT_URLCONF = 'seo_app.tenant_urls'
AUTH_USER_MODEL = "authorization_app.UserModel"
# Switch to authentication backend django-tenants-user
AUTHENTICATION_BACKENDS = (
"tenant_users.permissions.backend.UserBackend",
)
TENANT_USERS_DOMAIN = "127.0.0.1"
MIDDLEWARE = [
# 'seo_app.middleware.TenantSubfolderMiddleware',
'django_tenants.middleware.TenantSubfolderMiddleware', # Set tenants - subfolder
# 'django_tenants.middleware.main.TenantMainMiddleware', # Set tenants - subdomains
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
INTERNAL_IPS = [
'127.0.0.1',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request', # DJANGO TENANTS
'django.template.context_processors.debug',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'seo_app.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend', # TENANTS CONFIGURATION
'NAME': 'xxxxxx', # Name of your database
'USER': 'lukas_db', # Database user
'PASSWORD': 'xxxxxxxxx', # User password
'HOST': '127.0.0.1', # Server address (or IP address)
'PORT': '5432', # Port on which PostgreSQL is running (5432)
}
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'cs'
LOGOUT_REDIRECT_URL = '/'
LOGIN_REDIRECT_URL = '/dashboard/'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# settings.py
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_URL = '/media/'
# asynchronous processing
CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' # Uses Redis on localhost
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Prague'
CELERY_WORKER_POOL = 'prefork'
CELERY_WORKER_POOL = 'solo'
STATIC_URL = "/static/"
STATICFILES_DIRS = [
BASE_DIR / 'static',
# Add more paths if needed
]
# STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SESSION_COOKIE_DOMAIN = '127.0.0.1'
SESSION_COOKIE_NAME = 'sessionid_tenant'
CSRF_COOKIE_DOMAIN = "127.0.0.1"
SHOW_PUBLIC_IF_NO_TENANT_FOUND = True
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True
# TENANTS CONFIGURATION
DATABASE_ROUTERS = (
'django_tenants.routers.TenantSyncRouter',
)
TENANT_SUBFOLDER_PREFIX = "client"
Thank you very much for any advice you can offer. I’ve been working on this for several days without any success. Unfortunately, I couldn’t find any guidance in the documentation on how to implement a login system where the user can log in easily and then be redirected to their own schema, where they would use only their own database.
I tried writing new middleware.
I tried changing URL paths.
I tried using subdomain-based django-tenant. Unfortunately, there were also issues with cookies being lost. On MacOS, I couldn’t configure this in /etc/hosts. Authentication always occurs in the public schema, and when redirecting to the subdomain, all cookies are lost.
Several methods of logging and rewriting views. First, authentication and then redirection took place. In another attempt, I set the context using with schema_context. The latest approach, which you can see in the shared code, was supposed to identify the tenant based on the provided email, then find the corresponding subfolder in the database and create a path with /prefix/subfolder/tenant/login. The login to the tenant was supposed to happen on this page, but it doesn’t.
I also tried various configurations in settings.py, as it seems that some crucial information is missing from the documentation.
Thank you for any advice.
2
Answers
User amnesia with Django_tenants.
Dear @user25066324,
Have you found a solution yet?
The problem is that simply a lot of code lacks in Django_tenants. First of all, Django_tenants preconizes the use of subdomains for its tenants. There is indeed also a possibility to go for the use of subfolders, but that is only a beta version and generates many errors.
Django_tenants works beautifully with me, but not after I have spent about two months of research and work to make that happen.
There are three problems to solve:
I first had the subfolders working, but still kept having “user-amnesia” with Django_tenants, i.e. the user could log in and was accepted, but at the next page the user had become Anonymous again. For pages that required authorization, Django_tenants kept therefore running around.
There is a huge mass to cover here, so let’s kick off right away.
Let’s first start with the
proper management of the subfolders.
New middleware needs to be written. The beta version of the subfolder middleware of the Django_tenants does not suffice. Normally, you’re supposed to just switch the middleware in settings.py to the the subfolder middleware. This doesn’t work and I wrote my own middleware: subfolderhans.py, which I include here and you’re free to use. I have put this in the general section of my Django project, and refer to it in my settings.py like this:
Then, there is the urlresolver.py, also added to this answer, and that you have to write over the original one in the Django_tenant source files:
COPY urlresolvers.py /usr/local/lib/python3.10/dist-packages/django_tenants/urlresolvers.py
(on Linux), on Windows it is the
c:users<your name>AppDataRoaming...
Pay attention, AppData is a hidden directory, you have to put in your view settings “Show hidden files” to True.
Then, there is the context_processors.py, also included here and that you have to put in your general section of your Django_tenants project. You can use this if you want to have in your static map a directory per app, and then refer in your template files to images etc by using the context_processors.py. Personnaly, I don’t have so many graphics, so I put them all together and just make sure the names of the images are unique.
Second , the context_processors is also used in the settings.py:
Csb_tenants in my case is my tenant_app.
For the sake of completeness, here a copy of my shared and tenant app settings in settings.py:
I specifically didn’t use “tenant” as the tenant prefix, for obvious reasons.
Furthermore, I wrote a small procedure in a global.py file to get the tenant primary key from the url, which is more or less the same as the get_subfolder(request) function in the context_processors.py:
So, this for so much as the subfolder treatment.
Now the next step:
Authentication in Django_tenants with subfolders
Authentication goes wrong in django_tenants with subfolders. You need an extra layer of authentication in settings.py:
Both of the authentications will be executed, first the above one, then the latter one, and if one of those give a positive authentication, the authentications goes through. My TenantAuthenticationBackend will do the authentication for tenants, and if that doesn’t work, the classic Modelbackend can try for the public schema, and if that also doesn’t work, then the user clearly is not authorized.
This is the code for the TenantAuthenticationBackend, that I have put in the views of my model app, but that’s personal.
As you can see, you have to inherit from the classic Modelbackend, and then override the authenticate function. (I should say “method”, I know, but I’m from the old generation of IT engineers, when object orientation theorema was not commonly used but just started).
This authentication backend is key. Now we can use it a bit everywhere, most importantly with logging in, but also for the finding of the next page after the logging in:
Here you see the index method that is of course the first page of the model_app, and to have access, the user should have to log in. This logging in happens in the TenantLoginView (you called it the CustomLoginView), inherited and overridden for the classic login view. The secret is indeed, as you supposed already, the use of schema_context.
Having covered the authentication, we’re not there yet, since we now have to cover the third element, the sessions.
Django_tenant sessions management with subfolders.
Let’s make this clear, we’re not talking about the network sessions, but the sessions from Django. These sessions are written in the database, in a table called django_session, and there should be for every tenant and therefore schema a table called Django_session. This is where it goes wrong in the standard Django_tenants, where those tables are generated, but then the records are fetched from the public schema, where the particular records aren’t present.
You do not have to use the database to store the sessions, you can choose not to store them at all, or have them saved differently, but standard is the database.
A Django sessions holds a record of three fields, the first being a unique id, the second the payload and the third a timestamp. The unique Id and the payload (generally a dictionary that can hold a lot of data) are stored in the db fully encrypted. This is to cover for level 4 of confidentiality, where standing data also needs to be encrypted.
I wrote a middleware to manage those sessions, that I enclose to this answer again, that I have put in the general section of my django project and did like this in my settings.py:
‘SessionMiddlewareHans.TenantSessionMiddleware’,
#’django.contrib.sessions.middleware.SessionMiddleware’,
Since the order of these middlewares is important, because if the tenant is not known yet, then you can not authenticate, and if you have not authenticated, you should not go and fetch the session, etc. This is my order of middlewares in settings.py:
This will depend of course of your needs. I give it so you can compare. However, this order works for me.
This is pretty much it.
You will have to adapt a bit my code to your specific situation. Csb_tenants is the main tenant app, csb_tenant is the model app, csbtenants is the main model of the main tenant app, csbtenant is the model of the model app.
If I have forgotten something, or it is not clear, then please feel free to ask.
Django_tenants is extremele valuable software, and I’m happy to be able to contribute in this way.
Hans
Update: since it turns out I cannot upload codefiles, I give them here:
urlresolvers.py:
subfolderhans.py:
SessionMiddlewhareHans.py:
and finally, context_processors.py:
hope you are good. I have been trying to implement a multi tenant architecture authentication but I keep having errors. Do you have a tutorial or something that can assist me. Thanks in advance sir.