How to use django session authentication in multi tenant architecture?

In my multi-tenant web application, I have the following models

  1. MyUser Model - (AUTH_USER_MODEL which is an extension of default User model)
  2. Organization Model
  3. Account Model
class MyUser(AbstractBaseUser):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    is_staff = models.BooleanField(default=False)
    is_active = models.BooleanField()
    is_superuser = models.BooleanField()

class Organization(models.Model):
    id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
    name = models.CharField(max_length=200)
    owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='organization_owner')

class Accounts(PermissionsMixin, models.Model):
    is_superuser = None
    id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
    vk_user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='organization_member_set')
    is_active = models.BooleanField(default=False)

A single user can have many accounts. And there will be two types of login, the first when user logs into the website using his email and password, and the other when user logs in into any of his account using the account id and the cookie from the previous login. Login using email and password will happen only once, and on the other hand, user can switch between his accounts anytime after authentication. Different accounts can have different permissions. Each account has its own session and the request.user attribute will change depending on which account the user has logged in to. By that way we can call request.user.has_perm(‘some_perm_string’) to check for permissions on the account level.

I used the django auth user model to store the user’s email and password and use it only for first type login and it does not inherit the permissions mixin. And for accounts, I have an account model which does inherit permissions mixin. It is used for second type log in and relating each account to its set of permissions.

So how can django sessions can handle these two types of authentication?

Edit: The project I am working on is a fork of an open source project, CVAT.
Permissions are managed by third party package called django-rules. The link to the permissions file - https://github.com/openvinotoolkit/cvat/blob/develop/cvat/apps/authentication/auth.py

Right off-hand it looks like you’re going to need to either supplement or replace both django.contrib.sessions.middleware.SessionMiddleware and django.contrib.auth.middleware.AuthenticationMiddleware.

(And there may be more classes or modules needing to be modified or replaced - how Django manages the session associated with the session cookie is woven pretty deep into django.contrib.auth.)

@KenWhitesell I think we need more information. Because wouldn’t that (changing the middlewares) only be necessary if the multi-tenant architecture was extended to the domain level or sever level? If it’s still a single domain, but only the DB uses a multi-tenant design, the authentication should only require changing the authentication backend per Customizing authentication in Django | Django documentation | Django?

The issue I’m focusing on here is this:

(Emphasis added)

Yes, I could be misinterpreting this, but as I read it, it looks to me to be a “two-stage” login. The basic login to authenticate to the “system”, and then a subsequent login - keeping the same credentials and fundamental access - to each “site account”.

Since they want to switch these on-the-fly, I see the need to effectively replace all of the session id cookie handling for this.

Yes, I have planned it to have a two stage login, but it seems like it is not a good idea to extend many of the Django’s core modules to achieve this. I seems a overkill to maintain separate session and change request.user attribute for each of the user’s account just to mange the account permissions. Is there a other a other way around to check permissions based on the account the user has logged into?

Can we just store the current account details in the session data and change that when user switches account and add another attribute to request object, like request.account from which we can check the permissions?

I agree completely - I would never recommend doing it that way. But that’s what you defined as a requirement.

First, I’d suggest working on changing some terminology to avoid confusion. Call it whatever you want, but if a person is “authenticating” one time, that is their login. Find a different term to refer to their different “accounts” they will be using. (And if you want to call it “account”, that probably works well enough.)
Store the “current account” in session, and when it comes to checking permissions, check permissions to their “current account” and not their login. That’s going to be a whole lot easier than trying to maintain one login with multiple independent sessions. Keep it simple and avoid creating confusion by trying to mix two separate features of your system. Now, this probably means you’ll be implementing your own security layer, but that is going to be a lot easier than trying to do what you wanted to do originally.

@KenWhitesell @g-kartik i was wondering what got implemented in the end?

I’ve been working on a similar design issue. Single user account for logging in (based on AbstractUser), then a tenant-based Account type model that is used for tenant specific properties, etc. Users can in theory switch between tenants in a single session (similar to Asana or Sentry or others). The challenge is making use of permissions.

In theory, to keep separation of permissions between tenants (ie a user having different roles on different tenants), all permissions need to be checked against the Account model, not the User model. I’ve somewhat hacked this together with a custom authentication backend, copying many of the PermissionMixin methods into the Account model and then overrides the auth context processor (to replace the perms generated on the user), but it’s really hacked together, and fails for other third-party things that need a user object (like django-guardian).

Any suggestions on methods for rebuilding the security/permission structure to work off the Account model? I thought of having that model inherit from AbstractUser as well, but for modules that rely on the specified user model it would still fail.

Again, the specifics of this depend in some part on what you mean by “switch between tenants” and the precise differences in how the system is going to behave based on that.

Bun in the “base case”, if this concept of their “current tenant” is something stored in their session, then you don’t necessarily need to “rebuild” the security/permission structure.

You can use the user_passes_test decorator (or mixin) to perform your permissions check. If you’re using CBVs where the request object is directly available to you in the view, you can access that user’s “current tenant” directly. If you’re using FBVs, you can create your own version of that decorator (found in django.contrib.auth.decorators) to pass the request to the test_func function.

You can then test the permissions directly in the test_func, or you can integrate it with a custom has_perm-style function in the User model.

Having said all this, I’ll repeat though that the issue of a “row-level permissions”-type of security structure can be a very intricate issue. There’s no “one-size fits all” type of solution, and the proper answer for your specific use-case must be derrived from the specifics of your requirements. It’s not something for which you can assume an “off-the-shelf” solution is going to work for you.

The problem with user_passes_test and standard permissions in this context were, for example:
User Bob is a member of TenantA and TenantB by way of the Account model.

TenantA assigned Bob add_importantmodel permission through a group or even directly. When Bob activates TenantB, the same permission will exist as the default permission model has no concept of TenantA or TenantB, just a user and a permission.

To get around that, I’ve found ways to assign permissions to Account instead of User and then customized permission_required and user_passes_test to accept an Account rather than User.

Basically, in a multitenant system where one account can access multiple tenants, the concept of User needs to be split in half with the authentication user being different from the authorization user, and then permissions to follow. Arguably, you could make a tonne more permissions, or find some way to switch the User to one with the specific permissions, kind of how django-hijack works. So when switching tenants it switches to a tenant specific account.

Anyway, I wasn’t sure if there was a best practice, or a well documented way to get this done. Row-level is a pain because once you do something custom like this you do have to build a completely custom solution for that too.

You describe the situation like this:

But then go on to say:

Which is almost exactly what I recommended above.

But you don’t even need to go quite as far as you’ve described.

You could create Account as the M2M join table between User and Tenant. The permissions assigned to Account (another M2M relationship) then define the “role” that the user has with that tenant.

A custom user_passes_test function then is all that’s needed to make the permissions determination.

I have never found this to be necessary under any circumstance. Don’t conflate those two functions (authentication and authorization). Authentication is identification of an individual. Authorization is the determination of permissions within a given context. A comprehensive role-based security model is sufficiently flexible to address these requirements.

1 Like