I have a working multitenancy app in Django with isolated databases that currently uses a subdomain for each tenant and threading. The subdomain prefix is used to make a connection to the corresponding database, so:
- client1.mysite.com access the database client1
- client2.mysite.com access the database client2
And so on. This is the current code I have:
middleware:
import threading
from app.utils import tenant_db_from_the_request
Thread_Local = threading.local()
class AppMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
db = tenant_db_from_the_request(request)
setattr(Thread_Local, 'DB', db)
response = self.get_response(request)
return response
def get_current_db_name():
return getattr(Thread_Local, 'DB', None)
utils:
def hostname_from_the_request(request):
return request.get_host().split(':')[0].lower()
def tenant_db_from_the_request(request):
hostname = hostname_from_the_request(request)
tenants_map = get_tenants_map()
return tenants_map.get(hostname)
def get_tenants_map():
return dict(Tenants.objects.values_list('subdomain', 'database',))
routers:
class AppRouter:
def db_for_read(self, model, **hints):
return get_current_db_name()
def db_for_write(self, model, **hints):
return get_current_db_name()
def allow_relation(self, *args, **kwargs):
return True
def allow_syncdb(self, *args, **kwargs):
return None
def allow_migrate(self, *args, **kwargs):
return None
Using subdomains is not exactly ideal, so I’m trying to switch the subdomain prefix to a suffix that will be added to the username of each tenant. With that, every tenant will access the same URL:
mysite.com
And the suffix will be added to the username like that:
- user@client1 access the database client1
- user@client2 access the database client2
Since I already have this working with subdomain, my thought process is to change just the middleware to retrieve the suffix of the username and use session to store the tenant alias that will be used on each database request. With that in mind, I changed just my utils file to this:
def hostname_from_the_request(request):
# changed the request to get the 'db_alias' from the session
return request.session.get('db_alias', None)
def get_tenants_map():
# change the mapping to retrieve the database based on a alias, instead of subddomain
return dict(Tenants.objects.values_list('alias', 'database',))
And added the 3 lines of code before the authentication process in my login view:
if request.method == "POST":
if form.is_valid():
# retrieve the alias from the username
complete_username = form.cleaned_data.get('username')
db_alias = complete_username.split('@')[1]
request.session['db_alias'] = db_alias
user = authenticate(
username=form.cleaned_data.get('username'),
password=form.cleaned_data.get('password'),
)
This works, but only after the second login attempt. The first one always return a failed authentication error. At first I thought that this was because the session was being created only after the failed login attempt, but with print statements and breakpoints I can confirm that this is not the case. The session is being created before the authentication and the name of the database is being retrieved correctly (the name retrieval is correct because after the second attempt, the login is successful and each request is processed on the corresponding database).
I’ve tried to change the middleware settings and put the AppMiddleware right after the session middleware (before, it was the last on the list), but with no change.
I also tried to use the session data directly instead of threading, but the problem with that is that the router doesn’t accept a request parameter, or at least it is what I understood from the docs, but I could be wrong.
I’m not very skilled in Python / Django so maybe I’m missing something obvious. Or maybe this is the wrong way to achieve what I want. So, I’m missing something? How can i make this work with the first login attempt?