Django - multiple users with unique data - django

I'm building a SaaS that is something like a property maintenance application. A property maintenance company would use my application to manage multiple buildings and a whole bunch of tenants. So we have buildings and tenants all managed by one management company. However, a management company might have multiple staff (users) who are all managing these buildings/tenants.
Since this is a SaaS, I want to have a whole bunch of management companies all use my application to manage their own set of buildings/tenants. Thus, each management company needs to only be able to see their data and have no way to access another management company's data.
I have no idea how I might accomplish this in Django, though I have an idea:
Perhaps have a model called something like ManagementCompany and all buildings and tenants have a ForeignKey field which points to that ManagementCompany model. The staff of that management company would be connected to the ManagementCompany record via ForeignKey. In this way, the users can only see buildings/tenants whose ManagementCompany matches the users's.
Would that be a reasonable approach, or is there a better way?

Have you thought of subdomains?
Custom subdomains are a great way to provide customization for customers of SaaS products and to differentiate content without resorting to long URL paths.
So when a company registers, you either take the company name call to_lower() method, or prompt them to choose the one they like, just like Slack does, or Jira. I suggest you read this article Using Subdomains in Django Applications

It depends on how you want to store data in you SaaS application - single db for all instances or multiple db (each instance have separate database). Multiple db approach is pain when you want add new features, migrations etc. Single db is easier to manage but you need to add bunch of ForeignKey for each model.
For single db you will need:
Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).
Database router that will return database name for read/write depending on SaaS instance.
That's all. Django will read/write to separate databases.
For multiple db you will need:
Middleware that will detect SaaS instance (by subdomain, domain, port, custom url etc.).
Because you probably don't want to add ForeignKey to each model manually and filter it manually:
Abstract model with ForeignKey and custom save method to auto set that ForeignKey.
Custom model manager with custom get_queryset method that will filter all ORM queries with current SaaS instance. This manager should overide create method to auto set ForeignKey for queries like this: Foo.objects.create(**data)
Each model that will be fitlered for SaaS instance should inherit from that abstract model and you will need to set this model manager to that custom model manager.
That's all. Django will filter you ORM queries for current SaaS instance.
Example middleware (uses Domain model to check if domain exists, if not you will get HTTP404):
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_thread_locals = local()
def get_current_saas_instance():
return getattr(_thread_locals, 'current_instance', None)
class SaaSSubdomainMiddleware(object):
def process_request(self, request):
_thread_locals.current_instance = None
host = request.get_host()
try:
domain = Domain.objects.get(name=host)
_thread_locals.current_instance = domain.company
except:
logger.error('Error when checking SaaS domain', exc_info=True)
raise Http404
Example abstract model:
class SaaSModelAbstract(Model):
SAAS_FIELD_NAME = 'company'
company = ForeignKey(Company, null=True, blank=True)
class Meta:
abstract = True
def save(self, *args, **kwargs):
from .middleware import get_current_saas_instance
self.company = get_current_saas_instance()
super(SaaSModelAbstract, self).save(*args, **kwargs)
Example model manager:
class CurrentSaaSInstanceManager(models.Manager):
def get_current_saas_instance(self):
from .middleware import get_current_saas_instance
return get_current_saas_instance()
def get_queryset(self):
current_instance = self.get_current_saas_instance()
if current_instance is not None:
return super(CurrentSaaSInstanceManager, self).get_queryset().filter(
**{self.model.SAAS_FIELD_NAME: current_instance})
return super(CurrentSaaSInstanceManager, self).get_queryset()
def create(self, **kwargs):
current_instance = self.get_current_saas_instance()
if current_instance is not None:
kwargs[self.model.SAAS_FIELD_NAME] = current_instance
instance = self.model(**kwargs)
self._for_write = True
instance.save(force_insert=True, using=self.db)
return instance
Example models:
class FooModel(SaaSModelAbstract):
# model fields, methods
objects = CurrentSaaSInstanceManager()
class BarModel(models.Model):
# model fields, methods
pass
Example queries:
FooModel.objects.all() # will return query with all objects for current SaaS instance
BarModel.objects.all() # will return all objects withoout SaaS filtering
# Create objects for SaaS instance:
FooModel.objects.create(**data)
# or:
foo = FooModel()
foo.save()
In both cases (single/multiple db) django admin will be working properly.
I'm not posted db router because implementation is trivial and all you need can be found in django docs.

You can surely use django-tenant-schemas
Edit:
Given that you mentioned in the comment to my original answer that the database being used is MySQL, django-tenant-schemas is useless in your case. How about using multiple databases with database routers, that ways there can be separate databases for every company and using database routers you can route request for your databases through it.
It might be overwork but you probably can figure out a slick way to do it.

Related

Django: Custom default Manager with user based queries for all views/interfaces (custom, admin, graphene, rest, ...)

In Django I'm trying to implement some kind of a "security middleware", which gives access to certain db information only, if the logged in user matches.
Up to now I found two approaches: middleware or custom function in manager (Both explained here Django custom managers - how do I return only objects created by the logged-in user?).
Example for cleanest solution: custom function
class UserContactManager(models.Manager):
def for_user(self, user):
return self.get_query_set().filter(creator=user)
class MyUser(models.Model):
# Managers
objects = UserContactManager()
# then use it like this in views
data = MyUser.objects.for_user(request.user)
However, this solution only works, if you have control over the code which invokes this custom function (here: for_user()).
But if you are using third parties apps like Django-REST, Graphene or AdminViews, they don't have the possibility to configure a specific query-func to use.
My goal:
Replace the default Manager
Add user-based filters to query_set
Use the model as normal in all app configurations
Idea (pseudo code!)
from django.x import current_user
class UserBasedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(author=current_user.name)
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=50)
objects = UserBasedManager() # The default manager.
Now I could use the model Book as normal in all extensions.
I know this solution would have some drawbacks, which are okay for me:
Can be only used when user is available (e.g. no direct script support)
Handling no or anonymous user is missing (left it away to keep example short)
My use case
I have a project, which shall give access to data by different interfaces (admin pages, custom views, rest, graphql).
As I have so many interfaces, I don't want to implement the access-rights in the views. This would cost too much time and is hard to maintain and the risk for security problems in one specific view/interface is too high.
Let's say I'm storing commits of git repositories in a database.
And the user shall get access to all commits, which are part of repos the user has READ access.
The question is: How can I implement this as a generic, view/interface independent solution?
Install django-threadlocals package and call get_current_user function to get the current user
from threadlocals.threadlocals import get_current_user
class UserBasedManager(models.Manager):
def get_queryset(self):
current_user = get_current_user()
return super().get_queryset().filter(author=current_user.name)

How can I override user.groups in a custom account model in Django to implement a "virtual" group?

I have a custom account class in a Django app using PermissionsMixin:
class Account(AbstractBaseUser, PermissionsMixin):
Our CMS calls various .groups methods on this class in order to ascertain permissions.
We essentially want to override the queryset that is returned from .groups in this custom Account class and to inject an additional group under specific conditions. (I.e. the user has an active subscription and we then want to return "member" as one of the groups for that user, despite them not actually being in the group.)
How should we handle this override? We need to get the original groups, so that basic group functionality isn't broken, then inject our "virtual" group into the queryset.
Override the get_queryset method ManyRelatedManager. An object of ManyRelatedManager class has access to the parent instance.
Code Sample:
def add_custom_queryset_to_many_related_manager(many_related_manage_cls):
class ExtendedManyRelatedManager(many_related_manage_cls):
def get_queryset(self):
qs = super(ExtendedManyRelatedManager, self).get_queryset()
# some condition based on the instance
if self.instance.is_staff:
return qs.union(Group.objects.filter(name='Gold Subscription'))
return qs
return ExtendedManyRelatedManager
ManyRelatedManager class is obtained from the
ManyToManyDescriptor.
class ExtendedManyToManyDescriptor(ManyToManyDescriptor):
#cached_property
def related_manager_cls(self):
model = self.rel.related_model if self.reverse else self.rel.model
return add_custom_queryset_to_many_related_manager(create_forward_many_to_many_manager(
model._default_manager.__class__,
self.rel,
reverse=self.reverse,
))
Associated the ExtendedManyToManyDescriptor with groups field when
the Account class is initialized.
class ExtendedManyToManyField(ManyToManyField):
def contribute_to_class(self, cls, name, **kwargs):
super(ExtendedManyToManyField, self).contribute_to_class(cls, name, **kwargs)
setattr(cls, self.name, ExtendedManyToManyDescriptor(self.remote_field, reverse=False))
Override PermissionsMixin to use ExtendedManyToManyField for
groups field instead of ManyToManyField.
class ExtendedPermissionsMixin(PermissionsMixin):
groups = ExtendedManyToManyField(
Group,
verbose_name=_('groups'),
blank=True,
help_text=_(
'The groups this user belongs to. A user will get all permissions '
'granted to each of their groups.'
),
related_name="user_set",
related_query_name="user",
)
class Meta:
abstract = True
Reference:
django.db.models.fields.related_descriptors.create_forward_many_to_many_manager
Testing:
account = Account.objects.get(id=1)
account.is_staff = True
account.save()
account.groups.all()
# output
[<Group: Gold Subscription>]
The groups related manager is added by the PermissionMixin, you could actually remove the mixin and add only the parts of it that you need and redefine groups:
class Account(AbstractBaseUser):
# add the fields like is_superuser etc...
# as defined in https://github.com/django/django/blob/master/django/contrib/auth/models.py#L200
default_groups = models.ManyToManyField(Group)
#property
def groups(self):
if self.is_subscribed:
return Group.objects.filter(name="subscribers")
return default_groups.all()
Then you can add your custom groups using the Group model. This approach should work fine as long it is ok for all parts that groups returns a queryset instead of a manager (which probably mostly should be fine as managers mostly offer the same methods - but you probably need to find out yourself).
Update
After reading carefully the docs related to Managers and think about your requirement, I've to say there is no way to achieve the magic you want (I need to override the original, not to add a new ext_groups set - I need to alter third party library behavior that is calling groups.) Without touch the Django core itself (monkey patching would mess up admin, the same with properties).
In the solution I'm proposing, you have the necessary to add a new manager to Group, perhaps, you should start thinking in override that third-party library you're using, and make it use the Group's Manager you're interested in.
If the third-party library is at least medium quality it will have implemented tests that will help you to keep it working after the changes.
Proposed solution
Well, the good news is you can fulfill your business requirements, the bad news is you will have code a little more than you surely expect.
How should we handle this override?
You could use a proxy model to the Group class in order to add a custom manager that returns the desired QuerySet.
A proxy manager won't add an extra table for groups and will keep all the Group functionality besides, you can set custom managers on proxy models too, so, its perfect for this case use.
class ExtendedGroupManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
# Do work with qs.
return qs
class ExtendedGroup(Group):
objects = ExtendedGroupManager()
class Meta:
proxy = True
Then your Account class should then have a ManyToMany relationship to ExtendedGroup that can be called ... ext_groups?
Till now you can:
acc = Account(...)
acc.groups.all() # All groups for this account (Django default).
acc.ext_groups.all() # Same as above, plus any modification you have done in get_queryset method.
Then, in views, you can decide if you call one or another depending on a condition of your own selection (Eg. user is subscribed).
Is worth mention you can add a custom manager to an existeing model using the method contribute_to_class
em = ExtendGroupManager()
em.contribute_to_class(Group, 'ext_group')
# Group.ext_group is now available.
If your CMS calls Account.groups directly I would investigate how to override a ManyRelatedManager since the account.groups is actually a django ManyRelatedManager object.
It can probably be achieved using django Proxy model for the Group.
One strategy to investigate would then be:
Add the virtual group to the groups in the database.
Override the ManyRelatedManager by changing get_queryset() (The base queryset)
Something like (pseudo code):
def get_queryset():
account = self.instance # Guess this exist in a RelatedManager
if account.has_active_subscribtion():
return self.get_all_relevant_groups_including_the_virtual_one()
return self.get_all_relevant_groups_excluding_the_virtual_one()
The key here is to get access to the current instance in the custom RelatedManager
This may be a useful discussion around custom related managers.
Subclass a Django ManyRelatedManager a.k.a. ManyToManyField
I do not recommend to try fiddle/monkey patch with the queryset itself since it very easy to break the django admin etc...

Returning related fields of a model instance

I am creating an app with a rest API that should return values for instances of objects based on the url given. Right now I have the API working using ModelViewSets of my objects for the API.
For example I have three objects, user, transactions, and goals.
As it stands I can go to /mysite/api/users and return a list of all users
I can also go to /mysite/api/users/1 to return just the user with the id '1'.
I can do something similar with transactions and goals.
What I'm looking to do is go to url /mysite/api/users/1/transaction/1/goal
to find the goal associated with the transaction for that user.
I've been scouring tutorials and am not sure what the right question is to ask in order to find something useful to learn how to do this. What is the correct way to go about setting up my rest api like this?
If I understand correctly, you want to create nested ressources.
If you are using Viewsets, then the ExtendedRouter class of the drf-extensions package will allow you to achieve this.
Drf-extensions documentation about this feature: https://chibisov.github.io/drf-extensions/docs/#nested-routes
There is also this module, who also offer the same features.
You can either use url params or query params to solve your issue. I will explain the URL params solution here,
serializers.py
#Write a Goal Serializer
urls.py
#change the URL according to your environment
url(r'^users/(?P<uid>[0-9]+)/transaction/(?P<tid>[0-9]+)/goal/$', GoalViewSet.as_view({'get': 'user_transaction_goal',}), name='user-transaction-goal'),
views.py
class GoalViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = Goal.objects.all()
def user_transaction_goal(self, request, uid, tid):
#assuming user is FK in transaction and transaction is a FK in goal
#modify the filter rule according to your model design
goals = Goal.objects.filter(transaction=tid, transaction__user=uid)
serializer = GoalSerializer(goals, many=False)
return Response(serializer.data)
As #clement mentioned you can also use plugins to handle this situation.

Dynamically created proxy models do not have listed permissions on admin site

Here is my models.py:
class Item(models.Model):
# ... some irrelevent fields ...
tags = models.ManyToManyField('Tag')
class Tag(models.Model):
name = models.CharField(max_lenght=30)
category_id = models.IntegerField()
Tag is actually a general-purpose name. Each item has many different type of tags - currently there are four types: team tags, subject tags, admin tags and special tags. eventually there will probably be a few more.
The idea is, they all have basically the same fields, so instead of having like 4 tables with manytomany relationship, and instead of adding a new column for Item whenever adding a new type, everything is called a 'tag' and it's very easy to add new types without any change to the schema.
Now to handle this in the admin.py I'm using dyamically created proxy models (based on this), as such:
def create_modeladmin(modeladmin, model, name = None):
class Meta:
proxy = True
app_label = model._meta.app_label
attrs = {'__module__': '', 'Meta': Meta}
newmodel = type(name, (model,), attrs)
admin.site.register(newmodel, modeladmin)
return modeladmin
class TagAdmin(models.Model):
def queryset(self):
return self.model.objects.filter(category_id = self.cid)
class TeamAdmin(TagAdmin):
cid = 1
class SubjectAdmin(TagAdmin):
cid = 2
# ... and so on ...
create_modeladmin(TeamAdmin, name='Teams', model=Tag)
create_modeladmin(SubjectAdmin, name='Subject', model=Tag)
#... and so on ...
This works great for me. However, different staff members need different editing permissions - one guy shouldn't access admin tags, while the other should only have access to edit subject-tags and team-tags. But as far as the admin site is concerned - the dynamic models do not exist in the permission list, and I can't give anyone permissions regarding them.
i.e. a user given all permissions on the list will still not have access to edit any of the dynamic models, and the only way to let anyone access them at all is to give him a superuser which obviously defies the point
I searched SO and the web and I can't anyone with a similar problem, and the docs don't say anything about this not in the dynamic models section or the proxy models section. so I'm guessing this is a different kind of problem. Any help would be greatly appreciated
UPDATE
So after some research into it, the answer was simple enough. Since permissions in django are objects that are saved to the database, what I needed to do was simple - add the relevent permissions (and create new ContentType objects as well) to the db, and then I could give people specific pemissions.
However, this raised a new question - is it a good convention to put the function that creates the permissions inside create_modeladmin as a find_or_create sort of function (that basically runs every time) or should it be used as an external script that I should run once every time I add a new dynamic model (sort of like how syncdb does it)?
And is there a way to also create the permissions dynamically (which seems to me like the ideal and most fitting solution)?
of course you can create permissions, django have django.contrib.auth.management.create_permissions to do this

10 sites through same codebase django MTI, ABCs or EAV

I have a django based web shop that has been evolving over the past year. Currently there's about 8 country specific shops running through the same code base, plus an API, and there's soon to be a B2B website, and a few more countries to add to the list.
Variations are needed in model structure, particularly around fields in address models, the account model, and so on.
To make matters a bit more complicated, the site is running multidb with each shop instance in a separate db. So I have a situation where I might have a base ABC model, e.g:
class Address(models.Model):
class Meta:
abstract=True
class Address_UK(Address):
class Meta:
db_table="shop_address"
class Address_IT(Address):
class Meta:
db_table="shop_address"
[etc]
Then code throughout the app to select the the correct model, e.g.
if countrysettings.country == "UK":
address = Address_UK()
elif countrysettings.country == "IT":
address = Address_IT()
The countrysettings.country is actually a separate settings class which subclasses threading.local and the country code, which also corresponds with a key in settings.DATABASES, is configured by a geolocation middleware handler. So the correct database is selected, and the model specific variations are reflected in each country database.
But there are problems to this approach:
It completely breaks syncdb and is no good for south, unless I hack ./manage.py so I can pass in the country db, instead of requiring the middleware to set it.
It seems messy. So much if countrysettings.country == "xx": code lying about, and so many sub classed models.
So I was thinking of using django-eav instead, but I foresee problems in the admin, and in particular field ordering. I know that django-eav will build a modelform for the admin that includes the eav fields, but I'd ideally want these to be displayed or hidden relevant to the country.
Also I've considered having a none abstract base class, e.g. Address, and then creating country specific variations where needed (e.g Model Table Inheritance). But then I foresee the base models getting overloaded with one2one fields to each model variant. But it would solve issues with the admin.
Another option might be to have an extra data field, and to serialise additional fields into json or csv or something and store them in this field.
I can think about a few others way to attack your problem. I believe option 1 or option 2 are the best, but one might choose option 3.
Option 1: One code base, One db, One django instance, Sites framwork: If you do not actually need a distinct db for each store, create all tables and/or all possible fields, and smartly use the sites framework for condour fields fields and models. For example: keep for each address a address_type field etc and use different fields (and tables) on the same db for each site. This makes your code more complicated but simplifies your IT a lot. Use this if the code changes between sites is very minimal. (btw - The json serialization is a good option for address).
Option2: One code base, Many DBs, Many django instances: Set many sites with the same code base but carefully use conditional settings and dynamic features of python to generate different models per site. Each site will have it's own settings-uk.py, settings-us.py etc., and will have it's own db and own models using dynamic models. For example:
from django.conf import settings
# ...
class Address(models.Model):
name = models.CharField(max_length=100)
if settings.country == "US":
state = models.CharField(max_length=2)
else:
country = models.CharField(max_length=100)
Other possible tricks for this method: Enable/disable apps via the settings; Crafting custom pythonpaths for appsin the wsgi script/manage.py script; use if settings.country=='us': import uk_x as x else: import us_x as x . See also: http://code.flickr.com/blog/2009/12/02/flipping-out/
Option3: Parallel code branches, Many DBs, Many django instances: Use git to keep a few branches of your code and rebase them with each other. Requires much more IT effort. If you are planning to have many db and many server, (and many developers?) anyway, you might find this useful.
Another options: One DB, many django instances custom settings.py per instance without sites framework.
Actually EAV can solve your issue. You can in fact use EAV to show fields that a specific to the country attribute of the current object.
Here is an example
class Country(models.Model):
name = models.CharField(_("country"), max_length=50
class Address(eav.models.BaseEntity):
country = models.ForeignKey(Country, related_name="country_attrs")
# EAV specific staff
#classmethod
def get_schemata_for_model(self):
# When creating object, country field is still not set
# So we do not return any country-specific attributes
return AddressEAVSchema.objects.filter(pk=-1).all()
def get_schemata_for_instance(self, qs):
# For specific instance, return only country-specific attributes if any
try:
return AdressEAVSchema.objects.filter(country=self.country).all()
except:
return qs
# Attributes now can be marked as belonging to specific country
class AdressEAVSchema(eav.models.BaseSchema)
country = models.ForeignKey(Country, related_name="country_attrs")
# Rest of the standard EAV stuff
class AdressEAVChoice(eav.models.BaseChoice):
schema = models.ForeignKey(AdressEAVSchema, related_name='choices')
class AddressEAVAttribute(eav.models.BaseAttribute):
schema = models.ForeignKey(AdressEAVSchema, related_name='attrs')
choice = models.ForeignKey(AdressEAVChoice, blank=True, null=True)
Here how to use it:
When you create Address attributes you now have also to specify which country they belong to.
Now, when you create new Address object itself (say in admin), save, and then go back editing it you see additional country-specific attributes that match objects country.
Hope this helps.