多租户django应用程序中的范围查询
django-scopes的Python项目详细描述
django范围
动机
我们中的许多人使用django构建多租户应用程序 获取对应用程序中一小部分数据的访问权限,而 同时拥有some全局功能,使每个 客户不可行。而django在保护我们不构建sql方面做得很好 注入漏洞和类似错误,django无法保护我们免受逻辑攻击 错误和多租户最危险的安全问题之一 应用程序是我们在租户之间泄漏数据。
很容易忘记一个.filter
调用,很难捕捉到这些错误
在手动和自动测试中,因为您通常没有很多客户机
在你的开发设置中。离开radical, database-dependent ideas
除此之外,生态系统中没有很多方法可以防止这些错误
除了严格的代码审查之外。
安装
除了简单的
pip install django-scopes
兼容性
这个库是针对python 3.5-3.7和django 2.1-2.2进行测试的。
用法
假设我们有一个多租户的blog应用程序,由三个模型组成Site
,
Post
,和Comment
:
fromdjango.dbimportmodelsclassSite(models.Model):name=models.CharField(…)classPost(models.Model):site=models.ForeignKey(Site,…)title=models.CharField(…)classComment(models.Model):post=models.ForeignKey(Post,…)text=models.CharField(…)
在这种情况下,我们的应用程序可能会充满如下语句
Post.objects.filter(site=current_site)
,Comment.objects.filter(post__site=current_site)
,
或者当涉及更灵活的权限处理时更复杂。使用django scopes,我们
使用自定义的基于权限的筛选器来编写这些查询,但是
我们添加了一个自定义模型管理器,该模型管理器了解作为
租户范围:
fromdjango_scopesimportScopedManagerclassPost(models.Model):site=models.ForeignKey(Site,…)title=models.CharField(…)objects=ScopedManager(site='site')classComment(models.Model):post=models.ForeignKey(Post,…)text=models.CharField(…)objects=ScopedManager(site='post__site')
关键字参数site
定义了scope维度的名称,而字符串
'site'
或'post__site'
告诉我们如何查找此作用域维度的值
在ORM查询中。
通过将多个关键字参数传递给
ScopedManager
,例如ScopedManager(site='post__site', user='author')
如果
与你的用例相关。
现在,有了这个自定义管理器,所有查询一开始都被禁止:
>>> Comment.objects.all()
ScopeError: A scope on dimension "site" needs to be active for this query.
唯一有效的方法是Comment.objects.none()
,这对django很有用。
通用视图定义。
在上下文中激活作用域
现在,您可以使用我们的上下文管理器专门允许查询特定的博客站点, 例如:
fromdjango_scopesimportscopewithscope(site=current_site):Comment.objects.all()
这将自动向所有查询添加一个.filter(post__site=current_site)
。
同样,我们建议您仍然显式地编写它们,但是很高兴知道
保护。
当然,您仍然可以显式地输入非作用域上下文来访问 系统:
withscope(site=None):Comment.objects.all()
这也可以正确地嵌套在以前定义的范围内。您还可以激活多个 值一次:
withscope(site=[site1,site2]):Comment.objects.all()
把那些with
语句放在任何地方听起来都很麻烦?可能一点也不:你可能
已经有了一个中间件,它可以为每个请求确定站点(或者通常是租户)
基于url或登录用户,您可以很容易地使用它来自动包装
它围绕着所有特定于租户的视图。
函数可以使用
fromdjango_scopesimportscopes_disabledwithscopes_disabled():…# OR@scopes_disabled()deffun(…):…
自定义管理器类
如果已经在使用自定义管理器类,则可以使用_manager_class
将其传递给ScopedManager
关键字如下:
来自django.db导入模型
fromdjango.dbimportmodelsclassSiteManager(models.Manager):defget_queryset(self):returnsuper().get_queryset().exclude(name__startswith='test')classSite(models.Model):name=models.CharField(…)objects=ScopedManager(site='site',_manager_class=SiteManager)
注意事项
我们希望在默认情况下强制作用域以保持安全,不幸的是 破坏django测试运行器以及pytest django。现在,我们还没有找到 一个比monkeypatch更好的解决方案:
fromdjango.testimportutilsfromdjango_scopesimportscopes_disabledutils.setup_databases=scopes_disabled()(utils.setup_databases)
当使用模型表单时,django将自动按语法在foreign上生成选择字段 键和多对多字段。这在这里不起作用,所以我们提供helper字段 使用 改为空的queryset:
fromdjango.formsimportModelFormfromdjango_scopes.formsimportSafeModelChoiceFieldclassPostMethodForm(ModelForm):classMeta:model=Commentfield_classes={'post':SafeModelChoiceField,}
我们注意到django-filter
在生成筛选器集时也运行一些查询。
目前,我们最好的解决方法是:
fromdjango_scopesimportscopes_disabledwithscopes_disabled():classCommentFilter(FilterSet):…