This is a pre-production deployment of Warehouse, however changes made here WILL affect the production instance of PyPI.
Latest Version Dependencies status unknown Test status unknown Test coverage unknown
Project Description

Table of Contents

This application expands the django users and permissions frameworks to work alongside django-tenant-schemas or django-tenants to allow global users with permissions on a per-tenant basis. This allows a single user to belong to multiple tenants and have different roles and permissions in each tenant, including allowing permissions in the public tenant. This app also adds support for querying all tenants a user belongs to, and what roles the user has in a tenant.

Overview

To simplify the use of Django’s models and ORM layer, we need to isolate each tenant so we don’t have to filter objects (queries) by tenant. Since we are using PostgreSQL we handle this by using a separate schema for each tenant. We use django-tenant-schemas or django-tenants to handle all of this. Using separate schemas per tenant provides a small layer of security, permissions isolation, and some performance benefits. On the flip side, we’ve now segregated the database completely on a per-tenant basis, and have no default support for global authentication. For instance, if one user belongs to multiple tenants they should not need to have multiple accounts and should not have to sign in multiple times (i.e. be forced to sign for each tenant). They also should be able to see all the tenants they belong to. By default, this is not possible with django-tenant-schemas or django-tenants so we have created a solution to this problem (described below).

The django middleware (django-tenant-schemas or django-tenants) handles the schema setting automatically on a per-request basis. It does this by looking at the subdomain that the request comes in on and maps that to a ‘tenant’. No subdomain, or the ‘www’ subdomain map to a default ‘public’ tenant. In order for this mapping to work to a tenant, we have to create the tenant and the tenant’s schema in the database.

We leverage the tenant schemas middleware to handle most of this transparently. However, to get global authentication (global user accounts) and per-tenant authorization (permissions) working we rely on some of the nuances of schemas.

A schema is quite similar to a search path on a file system “path1;path2;path3” that gets searched in order. If no model exists in the first schema then the next one is searched. If the model exists but is empty, it does not continue searching (a model was found). By default, the ‘public’ schema is always transparently appended to the end of the schema path whenever set_schema is called.

Global Authentication Solution

With this in mind, it’s easy to see that we can create a users table at the global level in the public schema but NOT in the tenants schemas and the users table will still be searched and found, regardless of which tenant is selected. However, the problem is that we need to create our permissions on a per tenant basis, not at a global level. In fact, since the public schema also represents the website, the permissions for a user at the global level should only reflect the permissions for the website (i.e. can I post on the blog?). It’s really an entirely different permission set at the public schema level that we need to support (different roles).

The difficulty comes in at the Django level. Luckily Django supports using a custom user model. However, internally, the way Django uses the model tightly couples aspects of the authentication (user/pass and profile) and authorization (permissions) together, despite each one of those aspects inheriting from a separate mixin (parent tree: see PermissionsMixin and AbstractUserBase classes). As an artifact of the coupling, the authentication/permissions backend, as well as other components, make the assumption that it is one one model in the database. The built in function get_user_model() returns the model that is configured as the user model (whether its the stock django user model or a custom user model). We handle this in a relatively simplistic way. First, we decouple the two components (user profile and user permissions) into UserProfile and UserTenantPermissions. We install the UserProfile only at the SHARED_APPs (public schema) level. We install the Permissions model at ALL levels – SHARED_APPs and TENANT_APPs (both public schema, and per tenant schema). Then we ‘facade’ the two together (see AbstractBaseUserFacade and TenantPermissionsMixinFacade classes) to make each model look and behave like it encapsulates all of the functionality of a single unified user model. The part that makes it a little more tricky (and perhaps more clever) is that the two models that are linked at any point in time for a query is defined by the currently set schema.

Let’s look at an example. When accessing the website (via a request), the public schema is set (because its the public tenant), then the public Permissions model located in the public schema is what gets looked up by permissions queries, as well as the public user model (which is also located in the public schema). When a request comes in for a tenant, “EvilCorp” the EvilCorp schema is selected automatically via the middleware. However, remember that the schema set is really a search path that also contains the public schema appended to the end (see above). Thus, when a query comes in looking up permissions, it finds a permissions model INSIDE the EvilCorp tenant, and uses those permissions (rather than the model in the public schema), but when it looks up the UserProfile, nothing exists in the EvilCorp tenant schema so it falls back and searches the public schema and finds the UserProfile there. So essentially we end up ‘glueing’ these components together at run time for any given query using the schema search path.

Next, we have to create a custom authentication backend to handle the new user/permission model segregation. Luckily this is fairly easy and almost requires no changes since both our models provide facade interfaces to each other! We don’t have to change any of the logic in the auth backend (in fact the permission caching still even works at a per-tenant level automagially!). The only change we make is slight, and that is the way the default backend uses get_user_model() to look up meta data about the user model. We just override methods using this functionality to change this behavior and force it to use the tenant permissions model for permissions meta work, instead of the user model thats returned from get_user_model().

Tenants are stored at the public schema level and are also what defines each tenant. Tenants and users are linked at the public level as well, so we can query a user and see what tenants it belongs to, or query a tenant and see what users are associated. However, the role and permissions of a user are defined inside the tenant’s schema itself, so to view that data, we have to switch the schema over (normally this happens automatically, but in the case of wanting to view a users roles on a public profile page, we would have to force set the schema (connection.set_schema(‘EvilCorp’), as defined in the Tenant model for that tenant). We also have to remember to set it back. Most use cases will not ever have to touch the schema setting directly.

User and Tenant ‘Deletion’

With this solution, we also implement an alternative to avoid actually deleting users or tenants, so we need a way to make them disappear into the ether (from the users perspective) without conflict (i.e. don’t allow a deleted tenant to permanently monopolize a tenant URL subdomain, and don’t allow a users email to never be used again for signup). To handle the user delete, we just set the user is_active/staff/superuser to false and delete all links to any tenants it owns, as well as all instances of permissions it has in any tenant it was associated with. A user can “delete” a tenant manually, or in the case that a deleted user owns a tenant, we “delete” the tenant. When we “delete” a tenant, we disassociate any users with any roles/permissions, and then change the owner of the tenant’s schema to the public schema’s owner (the same owner that was configured when create_public_tenant command was run). When we do this, we also rename the tenant’s URL to be ownerid-timestamp-originalurl. Not only does this encapsulate some of the history of the tenant’s ownership, but it also frees up the URL namespace. Also, we never have to worry about schemas in the database conflicting because when we generate a tenant’s schema, we append the timestamp (in seconds since the epoch) to the name. Thus, every schema ends up unique when made, eliminating any schema level conflicts.

To do a full delete on Users/Tenants the delete methods can be overridden, or force_drop=True can be passed in to delete.

User Roles

We leverage the built in django role (Group) and permission (Permission) classes that are inherited through the PermissionsMixin. However, with DRF (django rest framework) if you also want to support a ‘view’ permission in addition to the add/change/delete permissions (the default ones that are generated) then the view permission needs to be added for all ContentTypes.

One possibility is to use a post-migration hook to iterate over all ContentTypes and check if a ‘view’ permission exists for that content type. If it doesn’t create it. It is important to note that roles and permissions exist at a per tenant level, so it must be done for each tenant when it’s created or migrations occur.

We populate all the default roles during tenant provisioning. However, as noted above we support roles/permissions at a per tenant level, so there is flexibility of user defined roles and modified permissions.

Installation

Assuming you already have django-tenant-schemas or django-tenants installed and configured, the first step is to install django-tenant-users.

pip install django-tenant-users

Test Project

All of the following settings/configuration can be seen in the dtu_test_project located in the GitHub repository

Basic Settings

You’ll have to make the following additions to the SHARED_APPS and TENANT_APPS` in your ``settings.py file.

SHARED_APPS=[
    # ...
    'django.contrib.auth', # Defined in both shared apps and tenant apps
    'django.contrib.contenttypes', # Defined in both shared apps and tenant apps
    'tenant_users.permissions', # Defined in both shared apps and tenant apps
    'tenant_users.tenants', # defined only in shared apps
    'customers', # Custom defined app that contains the TenantModel. Must NOT exist in TENANT_APPS
    'users', # Custom app that contains the new User Model (see below). Must NOT exist in TENANT_APPS
    # ...
]

TENANT_APPS=[
    # ...
    'django.contrib.auth', # Defined in both shared apps and tenant apps
    'django.contrib.contenttypes', # Defined in both shared apps and tenant apps
    'tenant_users.permissions', # Defined in both shared apps and tenant apps
    # ...
]

You will have to set the TENANT_USERS_DOMAIN setting to the domain hosting the tenants. This is utilized in provision_tenant to fill out the domain_url to match incoming requests.

TENANT_USERS_DOMAIN = "example.com"

Modifying the Tenant Model

Next we need to modify the TenantModel, which you should already have configured in settings.py. We need to change the inerhitance chain to inherit from TenantBase (previously it was TenantMixin). Below is an example TenantModel located in the ‘customers’ app that we installed above in the basic configuration section. Note. this ‘customers’ should ONLY be installed in the SHARED_APPs list.

customers/model.py

from tenant_users.tenants.models import TenantBase

class Client(TenantBase):
    name = models.CharField(max_length=100)
    description = models.TextField(max_length=200)

The settings.py file entry should look like:

settings.py

TENANT_MODEL = 'customers.Client'

Creating the User Model

Now we need to do the same thing to the User model. If you are not using a custom user model, then one needs to be built and configured in settings.py. The custom user model needs to inherit from the tenant_users UserProfile model. Additional fields can then be added to your custom user model, if desired. In this example, we will add the TenantUser model to the users application that we installed above in the basic configuration.

users/models.py

from tenant_users.tenants.models import UserProfile

class TenantUser(UserProfile):
    name = models.CharField(
        _("Name"),
        max_length = 100,
        blank = True,
    )

The settings.py file entry would look like (see Django documentation for more details):

settings.py

AUTH_USER_MODEL = 'users.TenantUser'

Setting up the Authentication Backend

At this point we now have all of the user, permissions, and tenant models configured. Because Django does not completely isolate authorization (permissions) from authentication (user/pass) we have to use a minimally modified authentication backend. Switch the authentication backend as follows:

AUTHENTICATION_BACKENDS = (
    'tenant_users.permissions.backend.UserBackend',
)

Notes: If you want to use django admin you will have to utilize admin multisite. Warning: if you set this up incorrectly you could expose access to models that users are not permitted to access (due to the schema search path being present, and falling through. See notes in code). You must reset migrations after updating the user model.

Setting up cross domain cookies

Setting up cross domain cookies will allow a single sign on to access any of the tenants with the same session cookies.

SESSION_COOKIE_DOMAIN = '.mydomain.com'

Warning: read the django documentation to understand the impacts of using SESSION_COOKIE_DOMAIN

Creating a User

All users apart from the first public tenant user (see Migrating and Creating the Public Tenant for creating the first public tenant user) should be created through the object manager to handle all of the default roles and tenant permissions being created for the user.

user = TenantUser.objects.create_user(email="user@evilcorp.com", password='password', is_active=True)

Currently all users rely on an email for the username.

Provisioning a Tenant

Here is an example to provision a tenant with the url “evilcorp.example.com”. Note that we set the TENANT_USERS_DOMAIN above to example.com.

Note: the user with the specified email must exist before provisioning a tenant. That’s because users can exist without a tenant, but a tenant can’t exist without a user (owner).

from tenants.tasks import provision_tenant

fqdn = provision_tenant("EvilCorp", "evilcorp", "admin@evilcorp.com").

Since provisioning a tenant also has to create the entire schema – depending on the models installed, it can take a while. It is recommended that this does not occur in the request/response cycle. A good asynchronous option is to use a task runner like Celery (along with tenant-schemas-celery) to handle this.

Advanced Usage: Setting up default roles

By default all default roles are blank (no permissions). In settings.py populate the following to add default roles to the public tenant or tenants when they are created:

  • PUBLIC_TENANT_DEFAULT_ROLES
  • PUBLIC_TENANT_DEFAULT_ROLES[PUBLIC_ROLE_DEFAULT]
  • TENANT_DEFAULT_ROLES
  • TENANT_DEFAULT_ROLES[TENANT_ROLE_ADMIN]
TENANT_DEFAULT_ROLES = {
    TENANT_ROLE_ADMIN : [
        {
            'app': 'my_custom_tenant_app',
            'permissions': ['view', 'add', 'change', 'delete'],
        },
    ]
}

During the provision_tenant call the default roles will be created on the newly provisioned Tenant using the TenantBase.create_roles() function. It can also be used at any time to create additional role types on a TenantBase.

Note: the ‘view’ permission is not a default permission in Django, but is often added and used in the context of Django Rest Framework.

Migrate and Create the Public Tenant

Django tenant schemas requires migrate_schemas to be called and a public tenant to be created. Here is an example of creating the public tenant.

from tenant_users.permissions.roles import PUBLIC_TENANT_DEFAULT_ROLES, PUBLIC_ROLE_DEFAULT

# Create public tenant user. It does not go through object manager because public tenant
# does not exist yet
user = TenantUser.objects.create(email="admin@evilcorp.com", is_active=True)
user.set_password('password')
user.save()

public_tenant = Client.objects.create(domain_url='evilcorp.example.com', schema_name='public', name='Evilcorp Website')
public_tenant.create_roles(PUBLIC_TENANT_DEFAULT_ROLES)
# Assign default role (empty permission set by default) to public tenant owner
# and creates the tenant permissions for the user
public_tenant.assign_user_role(user, PUBLIC_ROLE_DEFAULT, True)
Release History

Release History

0.2.18

This version

History Node

TODO: Figure out how to actually get changelog content.

Changelog content for this version goes here.

Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Show More

0.2.17

History Node

TODO: Figure out how to actually get changelog content.

Changelog content for this version goes here.

Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Show More

Download Files

Download Files

TODO: Brief introduction on what you do with files - including link to relevant help section.

File Name & Checksum SHA256 Checksum Help Version File Type Upload Date
django-tenant-users-0.2.18.tar.gz (17.3 kB) Copy SHA256 Checksum SHA256 Source Oct 11, 2016

Supported By

WebFaction WebFaction Technical Writing Elastic Elastic Search Pingdom Pingdom Monitoring Dyn Dyn DNS HPE HPE Development Sentry Sentry Error Logging CloudAMQP CloudAMQP RabbitMQ Heroku Heroku PaaS Kabu Creative Kabu Creative UX & Design Fastly Fastly CDN DigiCert DigiCert EV Certificate Rackspace Rackspace Cloud Servers DreamHost DreamHost Log Hosting