Reputation: 2852
I've a django-multitenant SAAS with this tenant model:
class Company(TenantModel):
name = models.CharField(
max_length=100,
unique=True
)
slug = AutoSlugField(
populate_from='name'
)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
class Meta:
verbose_name_plural = 'Companies'
class TenantMeta:
tenant_field_name = 'id'
def __str__(self) -> str:
return self.name
I've setup a GraphQL api using graphene-django
In my User
model I've a ManyToManyField
so the user can manage one or more companies.
The goal is to have a company selector in the UI so the user can decide which company to manage.
In order to accomplish that, I'm sending the tenant_id
in the request headers and using a middleware (as suggested in the documentation) to execute the set_tenant_id()
function.
After trying many things I ended up with a setup that works perfectly in localhost:
class SetTenantFromHeadersMiddleware:
"""
This Middleware works together with the next one (MultitenantGraphQLMiddleware).
This one is responsible for setting the current tenant if the header was provided, this logic is here
because for some reason in the MultitenantGraphQLMiddleware things differ as that's a GraphQL middleware:
It gets executed once per field in the request and was randomly raising Company.DoesNotExist exception.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.path == '/graphql':
tenant_header = request.META.get('HTTP_N1_TENANT', None)
if tenant_header:
_, company_id = from_global_id(tenant_header)
try:
company = Company.objects.get(id=company_id)
set_current_tenant(company)
except Company.DoesNotExist:
# This will be handled in MultitenantGraphQLMiddleware by validating there's a
# current tenant before resolving. It's safe to pass at this point.
pass
response = self.get_response(request)
return response
class MultitenantGraphQLMiddleware:
"""
This middleware is responsible for validating there's a current tenant or selecting default
from user's companies.
"""
@property
def safe_mutations(self) -> list[str]:
return [
CreateCompany.__name__,
'TokenAuth',
'RefreshToken',
]
@property
def safe_queries(self) -> list[str]:
return ['Me']
def is_safe_operation(self, operation) -> bool:
is_mutation = operation.operation == OperationType.MUTATION
is_safe_mutation = is_mutation and operation.name.value in self.safe_mutations
is_query = operation.operation == OperationType.QUERY
is_safe_query = is_query and operation.name.value in self.safe_queries
return is_safe_mutation or is_safe_query
def resolve(self, next, root, info, **kwargs):
if self.is_safe_operation(info.operation):
set_current_tenant(None)
return next(root, info, **kwargs)
if not hasattr(info.context, '_tenant_verified'):
current_tenant = get_current_tenant()
if current_tenant:
if current_tenant.id not in info.context.user.companies.all().values_list('id', flat=True):
raise Exception('Provided company ID does not belong to authenticated user')
else:
company = info.context.user.default_company
if not company:
raise Exception('Cannot execute GraphQL operation as there is no current tenant')
set_current_tenant(company)
return next(root, info, **kwargs)
In settings.py
:
...
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django_multitenant.middlewares.MultitenantMiddleware',
'api.middleware.SetTenantFromHeadersMiddleware',
]
...
GRAPHENE = {
'SCHEMA': 'api.schema.schema',
'MIDDLEWARE': [
'api.middleware.MultitenantGraphQLMiddleware',
'api.middleware.LoginRequiredMiddleware',
'graphql_jwt.middleware.JSONWebTokenMiddleware',
]
}
...
After deploying changes to the server (running the application inside a Docker container with uwsgi) it behaves in a very weird way by retrieving objects associated to another company or an empty list. That happens randomly. This works perfectly in localhost.
For example when fetching transactions, this happens:
The immediate next request:
Upvotes: 0
Views: 87