Corvia / django-tenant-users

Adds global user authentication and tenant-specific permissions to django-tenants.
https://django-tenant-users.rtfd.io
MIT License
356 stars 67 forks source link

TenantAccessMiddleware & Authentification #666

Open killer24vrccp opened 2 months ago

killer24vrccp commented 2 months ago

I have a problem with TenantAccessMiddleware. When user authentificated in public after in my public I receive this error: 'WSGIRequest' object has no attribute 'tenant'


SHARED_APPS = [
    "django_tenants",

    "tenant_users.permissions",
    "tenant_users.tenants",

    'app.generic.business',  # Tenant Model
    'app.generic.account',  # User model

    'django.contrib.auth',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.admin',
    'django.contrib.staticfiles',
    'django.contrib.admindocs',
    'django.contrib.sitemaps',
    'django.contrib.contenttypes',
]

TENANT_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    "tenant_users.permissions",
]

MIDDLEWARE = [
    'django_tenants.middleware.main.TenantMainMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'django_user_agents.middleware.UserAgentMiddleware',
    'tenant_users.tenants.middleware.TenantAccessMiddleware',
]

AUTHENTICATION_BACKENDS = (
    # 'django.contrib.auth.backends.ModelBackend',
    "tenant_users.permissions.backend.UserBackend",
)

TENANT_MODEL = 'business.Business'

TENANT_DOMAIN_MODEL = 'business.Domain'

DATABASE_ROUTERS = (
    'django_tenants.routers.TenantSyncRouter',
)

Account model

class Account(UserProfile):
    first_name = models.CharField(max_length=50, verbose_name=_('First name'))
    last_name = models.CharField(max_length=50, verbose_name=_('Last name'))

    picture_profile = models.ImageField(upload_to='pool/', null=True, blank=True, verbose_name=_('Picture'))
    gender = models.IntegerField(default=Gender.MALE, choices=Gender.choices, verbose_name=_('Gender'), blank=True)

Tenant/Domain Model

class Business(TenantBase):
    name = models.CharField(_('Business Name'), max_length=255, unique=True)
    schema_name = models.CharField(max_length=63, unique=True, db_index=True,
                                   validators=[_check_schema_name], verbose_name=_('Schema Name'), blank=True)

    created_on = models.DateField(auto_now_add=True)

    def save(self, *args, **kwargs):
        # assignation to business_name slugify for schema_name field
        if not self.schema_name:
            self.schema_name = slugify(self.name)

        super().save(*args, **kwargs)

class Domain(DomainMixin):
    pass

When I'm on my tenant and I try to login i'm stay not authentificated. I have add this settings SESSION_COOKIE_DOMAIN = ".localhost". I'm in localhost for dev.

Dresdn commented 2 months ago

The django-tenants middleware is responsible for adding the tenant key on the request object.

There are reasons it won't do that (see the try/except earlier in the method), so I would suggest adding a debug point in the process_request() and see why that key isn't being set.

thubamamba commented 2 months ago

Hey @killer24vrccp, the error is caused by the ordering in the middleware (weird, I know :P). Just ensure the tenant_user middleware comes after the django auth middleware like so:

"django.contrib.auth.middleware.AuthenticationMiddleware",
"tenant_users.tenants.middleware.TenantAccessMiddleware",
Dresdn commented 2 months ago

Good catch @thubamamba! Yes, the tenant_users middleware requires the AuthenticationMiddle since it's checking for authentication status.

We should probably document this in the docs and middleware docstring. I did originally write this as a starting point since a few people were asking, but by no means is it meant to fit every use case.

killer24vrccp commented 2 months ago

Hey @thubamamba I have try but I have the same error. I don't knwo why.

image image

Environment:

Request Method: GET
Request URL: http://localhost:8000/en/

Django Version: 4.2.15
Python Version: 3.10.11
Installed Applications:
['jazzmin',
 'django_tenants',
 'tenant_users.permissions',
 'tenant_users.tenants',
 'app.generic.business',
 'app.generic.account',
 'django.contrib.contenttypes',
 'django.contrib.auth',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.humanize',
 'django.contrib.admin',
 'django.contrib.staticfiles',
 'django.contrib.admindocs',
 'django.contrib.sitemaps',
 'rosetta',
 'django_user_agents',
 'rest_framework',
 'rest_framework.authtoken',
 'app.public.user_sessions',
 'app.public.core',
 'app.public.changelog',
 'app.generic.address',
 'app.generic.module',
 'bootstrap5',
 'app.api',
 'django_htmx',
 'crispy_forms',
 'crispy_bootstrap5',
 'django_countries',
 'parler']
Installed Middleware:
['django_tenants.middleware.main.TenantMainMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.locale.LocaleMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'tenant_users.tenants.middleware.TenantAccessMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django_user_agents.middleware.UserAgentMiddleware']

Traceback (most recent call last):
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\tenant_users\tenants\middleware.py", line 44, in __call__
    if not self.has_tenant_access(request):
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\tenant_users\tenants\middleware.py", line 61, in has_tenant_access
    return request.user.tenants.filter(id=request.tenant.id).exists()

Exception Type: AttributeError at /en/
Exception Value: 'WSGIRequest' object has no attribute 'tenant'
Dresdn commented 2 months ago

Exception Value: 'WSGIRequest' object has no attribute 'tenant'

Did you check out my prior comment? That tenant attribute is added to the request object by django-tenants middleware.

I would bet something is not setup right with django-tenants as this line will cause request.tenant not to be created. For the hostname you're trying to navigate to, is the domain setup? Try this from the shell:

from django_tenants.utils import get_tenant_domain_model
hostname = "whatever-you're-using"
domain_model = get_tenant_domain_model()
domain_model.objects.select_related('tenant').get(domain=hostname)

If that doesn't come up with anything, you'll get the error you're seeing.

killer24vrccp commented 2 months ago

I don't know if that's can help me but I use this command for approvisionning public tenant : python .\manage.py create_public_tenant --domain_url public.localhost --owner_email admin@admin.com

killer24vrccp commented 2 months ago

I have setup few print : localhost

Debug:

localhost
Internal Server Error: /favicon.ico
Traceback (most recent call last):
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\django\core\handlers\exception.py", line 55, in inner
    response = get_response(request)
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\tenant_users\tenants\middleware.py", line 44, in __call__
    if not self.has_tenant_access(request):
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\tenant_users\tenants\middleware.py", line 61, in has_tenant_access
    return request.user.tenants.filter(id=request.tenant.id).exists()
AttributeError: 'WSGIRequest' object has no attribute 'tenant'
    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()
        try:
            hostname = self.hostname_from_request(request)
            print(hostname)
        except DisallowedHost:
            from django.http import HttpResponseNotFound
            return HttpResponseNotFound()

        domain_model = get_tenant_domain_model()
        try:
            tenant = self.get_tenant(domain_model, hostname)
            print(tenant)
        except domain_model.DoesNotExist:
            self.no_tenant_found(request, hostname)
            return

        tenant.domain_url = hostname
        request.tenant = tenant
        connection.set_tenant(request.tenant)
        self.setup_url_routing(request)
Dresdn commented 2 months ago

I'm confused - did you not try the commands I suggested?

I don't know if that's can help me but I use this command for approvisionning public tenant : python .\manage.py create_public_tenant --domain_url public.localhost --owner_email admin@admin.com

You're creating a "domain" with the value public.localhost.

I have setup few print : localhost

Debug:

localhost

The domain you're accessing is localhost. Run the commands in my prior comment, or even Domain.objects.all(). If you're accessing localhost but the domain is set to public.localhost, then the django-tenants middleware won't find a match and will not create request.tenant, hence the failure.

thubamamba commented 2 months ago

I managed to get this working, the issue was indeed that there was no tenant being passed to the request. So, the first thing you have to ensure to do is create a public tenant.. check out the django-tenant docs or the django-tenant-users.

killer24vrccp commented 2 months ago

@Dresdn Yes I have try. I have retry with python manage.py create_public_tenant domain_url localhost owner_email admin@admin.com And result of command:

>>> hostname = "localhost"            
>>> domain_model = get_tenant_domain_model()
>>> domain_model.objects.select_related('tenant').get(domain=hostname)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "D:\Projet\CVS_tenant\.venv\lib\site-packages\django\db\models\query.py", line 637, in get
    raise self.model.DoesNotExist(
app.generic.business.models.Domain.DoesNotExist: Domain matching query does not exist.
>>> 
Dresdn commented 2 months ago

Let's ensure we're on the same page. Can you confirm the following?

  1. You created the public tenant by using: python manage.py create_public_tenant --domain_url public.localhost --owner_email admin@admin.com I see you re-tried it, but was that a clean database?

  2. You're navigating to http://localhost:8000? If not, what "hostname" are you browsing to?

  3. Whatever that hostname is, is there a Domain object created for it? Update the script you tried in the last comment.

To your last note, if there is no Domain model, django-tenants isn't setup correctly. You can check by seeing what domains are there via Domain.objects.all() If you're trying to navigate to a URL that is not listed there, it will fail.

I would ensure you have everything setup as called out in the Installation Docs.

killer24vrccp commented 2 months ago

I have resolve the problem. I have juste use localhost then public.localhost. But raise a new problem. My tenant URL not work in my public tenant. Thats said NoReverseMatch 'interface' is not a registered namespace.

I use {% if request.tenant %}{% url 'interface.css-min %}{% endif %}.