In my multi-tenant web application, I have the following models
MyUser Model - (AUTH_USER_MODEL which is an extension of default User model)
Organization Model
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?
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?
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.
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.
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.