{ "info": { "author": "Raphael Michel", "author_email": "mail@raphaelmichel.de", "bugtrack_url": null, "classifiers": [ "Framework :: Django :: 2.1", "Framework :: Django :: 2.2", "Intended Audience :: Developers", "Intended Audience :: Other Audience", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7" ], "description": "django-scopes\n=============\n\n[![Build Status](https://travis-ci.com/raphaelm/django-scopes.svg?branch=master)](https://travis-ci.com/raphaelm/django-scopes) [![codecov](https://codecov.io/gh/raphaelm/django-scopes/branch/master/graph/badge.svg)](https://codecov.io/gh/raphaelm/django-scopes) ![PyPI](https://img.shields.io/pypi/v/django-scopes.svg)\n\nMotivation\n----------\n\nMany of us use Django to build multi-tenant applications where every user only ever\ngets access to a small, separated fraction of the data in our application, while\nat the same time having *some* global functionality that makes separate databases per\nclient infeasible. While Django does a great job protecting us from building SQL\ninjection vulnerabilities and similar errors, Django can't protect us from logic\nerrors and one of the most dangerous types of security issues for multi-tenant\napplications is that we leak data across tenants.\n\nIt's so easy to forget that one ``.filter`` call and it's hard to catch these errors\nin both manual and automated testing, since you usually do not have a lot of clients\nin your development setup. Leaving [radical, database-dependent ideas](https://github.com/bernardopires/django-tenant-schemas)\naside, there aren't many approaches available in the ecosystem to prevent these mistakes\nfrom happening aside from rigorous code review.\n\nWe'd like to propose this module as a flexible line of defense. It is meant to have\nlittle impact on your day-to-day work, but act as a safeguard in case you build a\nfaulty query.\n\nInstallation\n------------\n\nThere's nothing required apart from a simple\n\n\tpip install django-scopes\n\nCompatibility\n-------------\n\nThis library is tested against **Python 3.5-3.7** and **Django 2.1-2.2**.\n\nUsage\n-----\n\nLet's assume we have a multi-tenant blog application consisting of the three models ``Site``,\n``Post``, and ``Comment``:\n\n```python\nfrom django.db import models\n\nclass Site(models.Model):\n\tname = models.CharField(\u2026)\n\nclass Post(models.Model):\n\tsite = models.ForeignKey(Site, \u2026)\n\ttitle = models.CharField(\u2026)\n\nclass Comment(models.Model):\n\tpost = models.ForeignKey(Post, \u2026)\n\ttext = models.CharField(\u2026)\n```\n\nIn this case, our application will probably be full of statements like\n``Post.objects.filter(site=current_site)``, ``Comment.objects.filter(post__site=current_site)``,\nor more complex when more flexible permission handling is involved. With django-scopes, we\nengourage you to still write these queries with your custom permission-based filters, but\nwe add a custom model manager that has knowledge about posts and comments being part of a\ntenant scope:\n\n```python\nfrom django_scopes import ScopedManager\n\nclass Post(models.Model):\n\tsite = models.ForeignKey(Site, \u2026)\n\ttitle = models.CharField(\u2026)\n\n\tobjects = ScopedManager(site='site')\n\nclass Comment(models.Model):\n\tpost = models.ForeignKey(Post, \u2026)\n\ttext = models.CharField(\u2026)\n\n\tobjects = ScopedManager(site='post__site')\n```\n\nThe keyword argument ``site`` defines the name of our **scope dimension**, while the string\n``'site'`` or ``'post__site'`` tells us how we can look up the value for this scope dimension\nin ORM queries.\n\nYou could have multi-dimensional scopes by passing multiple keyword arguments to\n``ScopedManager``, e.g. ``ScopedManager(site='post__site', user='author')`` if that is\nrelevant to your usecase.\n\nNow, with this custom manager, all queries are banned at first:\n\n\t>>> Comment.objects.all()\n\tScopeError: A scope on dimension \"site\" needs to be active for this query.\n\nThe only thing that will work is ``Comment.objects.none()``, which is useful e.g. for Django\ngeneric view definitions.\n\n### Activate scopes in contexts\n\nYou can now use our context manager to specifically allow queries to a specific blogging site,\ne.g.:\n\n```python\nfrom django_scopes import scope\n\nwith scope(site=current_site):\n\tComment.objects.all()\n```\n\nThis will *automatically* add a ``.filter(post__site=current_site)`` to all of your queries.\nAgain, we recommend that you *still* write them explicitly, but it is nice to know to have a\nsafeguard.\n\nOf course, you can still explicitly enter a non-scoped context to access all the objects in your\nsystem:\n\n```python\nwith scope(site=None):\n\tComment.objects.all()\n```\n\nThis also works correctly nested within a previously defined scope. You can also activate multiple\nvalues at once:\n\n```python\nwith scope(site=[site1, site2]):\n\tComment.objects.all()\n```\n\nSounds cumbersome to put those ``with`` statements everywhere? Maybe not at all: You probably\nalready have a middleware that determines the site (or tenant, in general) for every request\nbased on URL or logged in user, and you can easily use it there to just automatically wrap\nit around all your tenant-specific views.\n\nFunctions can opt out of this behavior by using\n\n```python\nfrom django_scopes import scopes_disabled\n\n\nwith scopes_disabled():\n \u2026\n\n# OR\n\n@scopes_disabled()\ndef fun(\u2026):\n \u2026\n```\n\n### Custom manager classes\n\nIf you were already using a custom manager class, you can pass it to a `ScopedManager` with the `_manager_class`\nkeyword like this:\nfrom django.db import models\n\n```python\nfrom django.db import models\n\nclass SiteManager(models.Manager):\n\n\tdef get_queryset(self):\n\t\treturn super().get_queryset().exclude(name__startswith='test')\n\nclass Site(models.Model):\n\tname = models.CharField(\u2026)\n\n\tobjects = ScopedManager(site='site', _manager_class=SiteManager)\n```\n\nCaveats\n-------\n\nWe want to enforce scoping by default to stay safe, which unfortunately\nbreaks the Django test runner as well as pytest-django. For now, we haven't found\na better solution than to monkeypatch it:\n\n```python\nfrom django.test import utils\nfrom django_scopes import scopes_disabled\n\nutils.setup_databases = scopes_disabled()(utils.setup_databases)\n```\n\nWhen using model forms, Django will automatically generate choice fields on foreign\nkeys and many-to-many fields. This won't work here, so we supply helper field\nclasses ``SafeModelChoiceField`` and ``SafeModelMultipleChoiceField`` that use an\nempty queryset instead:\n\n```python\nfrom django.forms import ModelForm\nfrom django_scopes.forms import SafeModelChoiceField\n\nclass PostMethodForm(ModelForm):\n class Meta:\n model = Comment\n field_classes = {\n 'post': SafeModelChoiceField,\n }\n```\n\nWe noticed that ``django-filter`` also runs some queries when generating filtersets.\nCurrently, our best workaround is this:\n\n```python\nfrom django_scopes import scopes_disabled\n\nwith scopes_disabled():\n class CommentFilter(FilterSet):\n \u2026\n```\n\n\n", "description_content_type": "text/markdown", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/raphaelm/django-scopes", "keywords": "json database models", "license": "Apache License 2.0", "maintainer": "", "maintainer_email": "", "name": "django-scopes", "package_url": "https://pypi.org/project/django-scopes/", "platform": "", "project_url": "https://pypi.org/project/django-scopes/", "project_urls": { "Homepage": "https://github.com/raphaelm/django-scopes" }, "release_url": "https://pypi.org/project/django-scopes/1.2.0/", "requires_dist": null, "requires_python": "", "summary": "Scope querys in multi-tenant django applications", "version": "1.2.0" }, "last_serial": 5394882, "releases": { "1.0.0": [ { "comment_text": "", "digests": { "md5": "4a83bbd769bc4a7c3624d5e91c0347ae", "sha256": "5f6fd3bbd28df141f5bf5ce9d081511cafdd428cf7e4a8f92c37427bbc6d34d2" }, "downloads": -1, "filename": "django_scopes-1.0.0-py3-none-any.whl", "has_sig": false, "md5_digest": "4a83bbd769bc4a7c3624d5e91c0347ae", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 7984, "upload_time": "2019-05-12T12:43:00", "url": "https://files.pythonhosted.org/packages/89/ac/6e981eabba9aa05d9ef5ea8b4f9bf7ac2a8e23a98acf2c7142b6e8a08190/django_scopes-1.0.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "cae52b9f2759b6f110a2f1ee663464ac", "sha256": "f0e75bc54d86ebeacd5d3fc5d170dbaf1b67ff6830326fc8f5c648c86f62290d" }, "downloads": -1, "filename": "django-scopes-1.0.0.tar.gz", "has_sig": false, "md5_digest": "cae52b9f2759b6f110a2f1ee663464ac", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4746, "upload_time": "2019-05-12T12:43:02", "url": "https://files.pythonhosted.org/packages/ed/76/ab50d0416be1450c60ac51a1bb907ab7b5f56b3e99a33d55667642cbf4dd/django-scopes-1.0.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "4d36c8e6b67800cbabc1fc12aaddb9a6", "sha256": "9627c4c948c4164f06dc21a90a3d6358b3b9375ee7bfb000588fc91445214440" }, "downloads": -1, "filename": "django_scopes-1.0.1-py3-none-any.whl", "has_sig": false, "md5_digest": "4d36c8e6b67800cbabc1fc12aaddb9a6", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8040, "upload_time": "2019-05-12T14:08:29", "url": "https://files.pythonhosted.org/packages/24/2a/336eb66e4a213c7780aa26c1fc82a1f994580eb7ddd91bd72f719b56dc1f/django_scopes-1.0.1-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "72449ae748bf3b5c7f2a6a6f924b5aa9", "sha256": "ad6f9312d093e7a80f9712a5ddb42c983f96fb63c823cdb1085b88be4239c92d" }, "downloads": -1, "filename": "django-scopes-1.0.1.tar.gz", "has_sig": false, "md5_digest": "72449ae748bf3b5c7f2a6a6f924b5aa9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 4806, "upload_time": "2019-05-12T14:08:31", "url": "https://files.pythonhosted.org/packages/72/80/f6096a5a79d791bd4271aa720ac57b78cc2da1f3005b59c544222dd0b123/django-scopes-1.0.1.tar.gz" } ], "1.1.0": [ { "comment_text": "", "digests": { "md5": "41dadb3d3af1e349ca76a61e95a08047", "sha256": "bd83505404ff5855a58e4e80eaafdf7f5f63209582b3e4c0c5eb2145557bcda9" }, "downloads": -1, "filename": "django_scopes-1.1.0-py3-none-any.whl", "has_sig": false, "md5_digest": "41dadb3d3af1e349ca76a61e95a08047", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 8437, "upload_time": "2019-05-12T15:53:47", "url": "https://files.pythonhosted.org/packages/11/00/81a37f7385a1e84a3983ebf1a7ee1a110d40203bcd2481c5f4a36e926489/django_scopes-1.1.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "90358ab7ca1daef4db2462ac68006d1f", "sha256": "93dce2d8f83146a7cd2360a558634859af71c00f120814ae4cd63aa998f0b030" }, "downloads": -1, "filename": "django-scopes-1.1.0.tar.gz", "has_sig": false, "md5_digest": "90358ab7ca1daef4db2462ac68006d1f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 5208, "upload_time": "2019-05-12T15:53:49", "url": "https://files.pythonhosted.org/packages/d6/a0/e1e13cde72019909f3ce1c5fd8d94836f6d1628b1ebe8770bf288d579417/django-scopes-1.1.0.tar.gz" } ], "1.2.0": [ { "comment_text": "", "digests": { "md5": "36eba7bd6793fa8e21b3952bea090927", "sha256": "0cbfd95f1e4dcbeed7af603a91fb0f64de1419210449e8faad58bfa8c091d827" }, "downloads": -1, "filename": "django_scopes-1.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "36eba7bd6793fa8e21b3952bea090927", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 10837, "upload_time": "2019-06-13T08:34:14", "url": "https://files.pythonhosted.org/packages/f1/7c/19d70feb6dba60fc9b3d0d8763eead2c0ed309c9129da0eea08ae3ebbb42/django_scopes-1.2.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6ee27c1078d69bbeeaa8a9ddc42f20d1", "sha256": "d1dc86101175f99276e1612da4bda6b6c055ff7f7e0c946d0c46d0723f6e9bb0" }, "downloads": -1, "filename": "django-scopes-1.2.0.tar.gz", "has_sig": false, "md5_digest": "6ee27c1078d69bbeeaa8a9ddc42f20d1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 6008, "upload_time": "2019-06-13T08:34:16", "url": "https://files.pythonhosted.org/packages/bb/72/0920dac19558442ea7b60d611d6bf552e2b41807acf7fb783c359bcf25a2/django-scopes-1.2.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "36eba7bd6793fa8e21b3952bea090927", "sha256": "0cbfd95f1e4dcbeed7af603a91fb0f64de1419210449e8faad58bfa8c091d827" }, "downloads": -1, "filename": "django_scopes-1.2.0-py3-none-any.whl", "has_sig": false, "md5_digest": "36eba7bd6793fa8e21b3952bea090927", "packagetype": "bdist_wheel", "python_version": "py3", "requires_python": null, "size": 10837, "upload_time": "2019-06-13T08:34:14", "url": "https://files.pythonhosted.org/packages/f1/7c/19d70feb6dba60fc9b3d0d8763eead2c0ed309c9129da0eea08ae3ebbb42/django_scopes-1.2.0-py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "6ee27c1078d69bbeeaa8a9ddc42f20d1", "sha256": "d1dc86101175f99276e1612da4bda6b6c055ff7f7e0c946d0c46d0723f6e9bb0" }, "downloads": -1, "filename": "django-scopes-1.2.0.tar.gz", "has_sig": false, "md5_digest": "6ee27c1078d69bbeeaa8a9ddc42f20d1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 6008, "upload_time": "2019-06-13T08:34:16", "url": "https://files.pythonhosted.org/packages/bb/72/0920dac19558442ea7b60d611d6bf552e2b41807acf7fb783c359bcf25a2/django-scopes-1.2.0.tar.gz" } ] }